diff --git a/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.py b/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.py index 08ba22de9a..6825725a52 100644 --- a/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.py +++ b/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.py @@ -8,7 +8,7 @@ from prowler.providers.microsoft365.services.entra.entra_service import ( class entra_managed_device_required_for_authentication(Check): - """Check if Conditional Access policies deny access to the Microsoft 365 + """Check if Conditional Access policies enforce managed device requirement for authentication. This check ensures that Conditional Access policies are in place to enforce managed device requirement for authentication. """ diff --git a/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/__init__.py b/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.metadata.json b/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.metadata.json new file mode 100644 index 0000000000..871d9f318e --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "entra_managed_device_required_for_mfa_registration", + "CheckTitle": "Ensure that only managed devices are required for MFA registration", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "Conditional Access Policy", + "Description": "Ensure that only managed devices are required for MFA registration. This ensures that users enroll MFA using secure, organization-controlled devices.", + "Risk": "If users are allowed to register MFA on unmanaged or potentially compromised devices, attackers with stolen credentials may register their own MFA methods, effectively locking out legitimate users and taking over accounts. This increases the risk of unauthorized access, data breaches, and privilege escalation.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources select User actions and check Register security information. Under Grant select Grant access. Check Require multifactor authentication and Require Microsoft Entra hybrid joined device. Choose Require one of the selected controls and click Select at the bottom. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enforce MFA registration only from managed devices by requiring compliance through Intune or Entra hybrid join. This ensures that users enroll MFA using secure, organization-controlled devices.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-device-registration" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.py b/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.py new file mode 100644 index 0000000000..7fc446c956 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.py @@ -0,0 +1,72 @@ +from prowler.lib.check.models import Check, CheckReportMicrosoft365 +from prowler.providers.microsoft365.services.entra.entra_client import entra_client +from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + GrantControlOperator, + UserAction, +) + + +class entra_managed_device_required_for_mfa_registration(Check): + """Check if Conditional Access policies enforce MFA registration on a managed device. + + This check ensures that Conditional Access policies are in place to enforce MFA registration on a managed device. + """ + + def execute(self) -> list[CheckReportMicrosoft365]: + """Execute the check to ensure that Conditional Access policies enforce MFA registration on a managed device. + + Returns: + list[CheckReportMicrosoft365]: A list containing the results of the check. + """ + findings = [] + + report = CheckReportMicrosoft365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = "No Conditional Access Policy requires a managed device for MFA registration." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if ( + UserAction.REGISTER_SECURITY_INFO + not in policy.conditions.application_conditions.included_user_actions + ): + continue + + if ( + ConditionalAccessGrantControl.DOMAIN_JOINED_DEVICE + not in policy.grant_controls.built_in_controls + or ConditionalAccessGrantControl.MFA + not in policy.grant_controls.built_in_controls + ): + continue + + if policy.grant_controls.operator == GrantControlOperator.OR: + report = CheckReportMicrosoft365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports the requirement of a managed device for MFA registration but does not enforce it." + else: + report.status = "PASS" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' does require a managed device for MFA registration." + break + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py index 9b83f98fc4..7dc91746ee 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -119,6 +119,14 @@ class Entra(Microsoft365Service): [], ) ], + included_user_actions=[ + UserAction(user_action) + for user_action in getattr( + policy.conditions.applications, + "include_user_actions", + [], + ) + ], ), user_conditions=UsersConditions( included_groups=[ @@ -261,31 +269,6 @@ class Entra(Microsoft365Service): ) return conditional_access_policies - async def _get_organization(self): - logger.info("Entra - Getting organizations...") - organizations = [] - try: - org_data = await self.client.organization.get() - for org in org_data.value: - sync_enabled = ( - org.on_premises_sync_enabled - if org.on_premises_sync_enabled is not None - else False - ) - - organization = Organization( - id=org.id, - name=org.display_name, - on_premises_sync_enabled=sync_enabled, - ) - organizations.append(organization) - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - - return organizations - async def _get_admin_consent_policy(self): logger.info("Entra - Getting group settings...") admin_consent_policy = None @@ -323,6 +306,31 @@ class Entra(Microsoft365Service): ) return groups + async def _get_organization(self): + logger.info("Entra - Getting organizations...") + organizations = [] + try: + org_data = await self.client.organization.get() + for org in org_data.value: + sync_enabled = ( + org.on_premises_sync_enabled + if org.on_premises_sync_enabled is not None + else False + ) + + organization = Organization( + id=org.id, + name=org.display_name, + on_premises_sync_enabled=sync_enabled, + ) + organizations.append(organization) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return organizations + class ConditionalAccessPolicyState(Enum): ENABLED = "enabled" @@ -330,9 +338,14 @@ class ConditionalAccessPolicyState(Enum): ENABLED_FOR_REPORTING = "enabledForReportingButNotEnforced" +class UserAction(Enum): + REGISTER_SECURITY_INFO = "urn:user:registersecurityinfo" + + class ApplicationsConditions(BaseModel): included_applications: List[str] excluded_applications: List[str] + included_user_actions: List[UserAction] class UsersConditions(BaseModel): diff --git a/tests/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles_test.py b/tests/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles_test.py index ed30be2c7a..4492ac809c 100644 --- a/tests/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles_test.py +++ b/tests/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles_test.py @@ -7,8 +7,8 @@ from prowler.providers.microsoft365.services.entra.entra_service import ( ConditionalAccessPolicy, ConditionalAccessPolicyState, Conditions, - GrantControls, GrantControlOperator, + GrantControls, PersistentBrowser, SessionControls, SignInFrequency, @@ -84,7 +84,9 @@ class Test_entra_admin_mfa_enabled_for_administrative_roles: display_name="Disabled Policy", conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -158,7 +160,9 @@ class Test_entra_admin_mfa_enabled_for_administrative_roles: display_name="No Admin Roles Policy", conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -234,6 +238,7 @@ class Test_entra_admin_mfa_enabled_for_administrative_roles: application_conditions=ApplicationsConditions( included_applications=["MicrosoftAdminPortals"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -312,7 +317,9 @@ class Test_entra_admin_mfa_enabled_for_administrative_roles: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -389,7 +396,9 @@ class Test_entra_admin_mfa_enabled_for_administrative_roles: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -482,7 +491,9 @@ class Test_entra_admin_mfa_enabled_for_administrative_roles: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], diff --git a/tests/providers/microsoft365/services/entra/entra_admin_portals_role_limited_access/entra_admin_portals_role_limited_access_test.py b/tests/providers/microsoft365/services/entra/entra_admin_portals_role_limited_access/entra_admin_portals_role_limited_access_test.py index 506d4908ae..71eaaa4dd4 100644 --- a/tests/providers/microsoft365/services/entra/entra_admin_portals_role_limited_access/entra_admin_portals_role_limited_access_test.py +++ b/tests/providers/microsoft365/services/entra/entra_admin_portals_role_limited_access/entra_admin_portals_role_limited_access_test.py @@ -83,7 +83,9 @@ class Test_entra_admin_portals_role_limited_access: display_name="Test", conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=[], excluded_applications=[] + included_applications=[], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -157,6 +159,7 @@ class Test_entra_admin_portals_role_limited_access: application_conditions=ApplicationsConditions( included_applications=["MicrosoftAdminPortals"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -234,6 +237,7 @@ class Test_entra_admin_portals_role_limited_access: application_conditions=ApplicationsConditions( included_applications=["MicrosoftAdminPortals"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], diff --git a/tests/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled_test.py index b3458c217d..9c2cfa7aa1 100644 --- a/tests/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled_test.py +++ b/tests/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled_test.py @@ -85,7 +85,9 @@ class Test_entra_admin_users_sign_in_frequency_enabled: display_name="Test", conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=[], excluded_applications=[] + included_applications=[], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -159,7 +161,9 @@ class Test_entra_admin_users_sign_in_frequency_enabled: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -255,7 +259,9 @@ class Test_entra_admin_users_sign_in_frequency_enabled: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -348,7 +354,9 @@ class Test_entra_admin_users_sign_in_frequency_enabled: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -441,7 +449,9 @@ class Test_entra_admin_users_sign_in_frequency_enabled: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -537,7 +547,9 @@ class Test_entra_admin_users_sign_in_frequency_enabled: display_name=display_name, conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=["All"], excluded_applications=[] + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], diff --git a/tests/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled_test.py index fd6ff82a91..a1c6c48f78 100644 --- a/tests/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled_test.py +++ b/tests/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled_test.py @@ -84,7 +84,9 @@ class Test_entra_identity_protection_sign_in_risk_enabled: display_name="Test", conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=[], excluded_applications=[] + included_applications=[], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -160,6 +162,7 @@ class Test_entra_identity_protection_sign_in_risk_enabled: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -242,6 +245,7 @@ class Test_entra_identity_protection_sign_in_risk_enabled: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -324,6 +328,7 @@ class Test_entra_identity_protection_sign_in_risk_enabled: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], diff --git a/tests/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled_test.py index e1f7626701..ead5b42d61 100644 --- a/tests/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled_test.py +++ b/tests/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled_test.py @@ -84,7 +84,9 @@ class Test_entra_identity_protection_user_risk_enabled: display_name="Test", conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=[], excluded_applications=[] + included_applications=[], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -159,6 +161,7 @@ class Test_entra_identity_protection_user_risk_enabled: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -240,6 +243,7 @@ class Test_entra_identity_protection_user_risk_enabled: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -321,6 +325,7 @@ class Test_entra_identity_protection_user_risk_enabled: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], diff --git a/tests/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication_test.py b/tests/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication_test.py index 18fa79199b..d40986b56c 100644 --- a/tests/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication_test.py +++ b/tests/providers/microsoft365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication_test.py @@ -83,7 +83,9 @@ class Test_entra_managed_device_required_for_authentication: display_name="Test", conditions=Conditions( application_conditions=ApplicationsConditions( - included_applications=[], excluded_applications=[] + included_applications=[], + excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -157,6 +159,7 @@ class Test_entra_managed_device_required_for_authentication: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], @@ -238,6 +241,7 @@ class Test_entra_managed_device_required_for_authentication: application_conditions=ApplicationsConditions( included_applications=["All"], excluded_applications=[], + included_user_actions=[], ), user_conditions=UsersConditions( included_groups=[], diff --git a/tests/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration_test.py b/tests/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration_test.py new file mode 100644 index 0000000000..11899eb574 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration_test.py @@ -0,0 +1,293 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.microsoft365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + UserAction, + UsersConditions, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_entra_managed_device_required_for_mfa_registration: + def test_entra_no_conditional_access_policies(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration import ( + entra_managed_device_required_for_mfa_registration, + ) + + entra_client.conditional_access_policies = {} + + check = entra_managed_device_required_for_mfa_registration() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires a managed device for MFA registration." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_entra_managed_device_disabled(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration import ( + entra_managed_device_required_for_mfa_registration, + ) + from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name="Test", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=[], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.OR + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_managed_device_required_for_mfa_registration() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires a managed device for MFA registration." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_entra_managed_device_enabled_for_reporting(self): + id = str(uuid4()) + display_name = "Test" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration import ( + entra_managed_device_required_for_mfa_registration, + ) + from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[UserAction.REGISTER_SECURITY_INFO], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.MFA, + ConditionalAccessGrantControl.DOMAIN_JOINED_DEVICE, + ], + operator=GrantControlOperator.OR, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_managed_device_required_for_mfa_registration() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' reports the requirement of a managed device for MFA registration but does not enforce it." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[id].dict() + ) + + assert result[0].resource_name == display_name + assert result[0].resource_id == id + assert result[0].location == "global" + + def test_entra_managed_device_enabled(self): + id = str(uuid4()) + display_name = "Test" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_managed_device_required_for_mfa_registration.entra_managed_device_required_for_mfa_registration import ( + entra_managed_device_required_for_mfa_registration, + ) + from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[UserAction.REGISTER_SECURITY_INFO], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.MFA, + ConditionalAccessGrantControl.DOMAIN_JOINED_DEVICE, + ], + operator=GrantControlOperator.OR, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_managed_device_required_for_mfa_registration() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' does require a managed device for MFA registration." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[id].dict() + ) + + assert result[0].resource_name == display_name + assert result[0].resource_id == id + assert result[0].location == "global" diff --git a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py index 5816d78305..e9fb9fb2eb 100644 --- a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py @@ -19,6 +19,7 @@ from prowler.providers.microsoft365.services.entra.entra_service import ( SignInFrequency, SignInFrequencyInterval, SignInFrequencyType, + UserAction, UsersConditions, ) from tests.providers.microsoft365.microsoft365_fixtures import ( @@ -42,16 +43,6 @@ async def mock_entra_get_authorization_policy(_): ) -async def mock_entra_get_organization(_): - return [ - Organization( - id="org1", - name="Organization 1", - on_premises_sync_enabled=True, - ) - ] - - async def mock_entra_get_conditional_access_policies(_): return { "id-1": ConditionalAccessPolicy( @@ -61,6 +52,7 @@ async def mock_entra_get_conditional_access_policies(_): application_conditions=ApplicationsConditions( included_applications=["app-1", "app-2"], excluded_applications=["app-3", "app-4"], + included_user_actions=[UserAction.REGISTER_SECURITY_INFO], ), user_conditions=UsersConditions( included_groups=["group-1", "group-2"], @@ -117,6 +109,16 @@ async def mock_entra_get_admin_consent_policy(_): ) +async def mock_entra_get_organization(_): + return [ + Organization( + id="org1", + name="Organization 1", + on_premises_sync_enabled=True, + ) + ] + + class Test_Entra_Service: def test_get_client(self): admincenter_client = Entra( @@ -160,6 +162,7 @@ class Test_Entra_Service: application_conditions=ApplicationsConditions( included_applications=["app-1", "app-2"], excluded_applications=["app-3", "app-4"], + included_user_actions=[UserAction.REGISTER_SECURITY_INFO], ), user_conditions=UsersConditions( included_groups=["group-1", "group-2"],