diff --git a/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/__init__.py b/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json b/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json new file mode 100644 index 0000000000..ffd4e74d5e --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "entra_policy_guest_invite_only_for_admin_roles", + "CheckTitle": "Ensure that 'Guest invite restrictions' is set to 'Only users assigned to specific admin roles can invite guest users'", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Authorization Policy", + "Description": "Restrict invitations to users with specific administrative roles only.", + "Risk": "Restricting invitations to users with specific administrator roles ensures that only authorized accounts have access to cloud resources. This helps to maintain 'Need to Know' permissions and prevents inadvertent access to data. By default the setting Guest invite restrictions is set to Anyone in the organization can invite guest users including guests and non-admins. This would allow anyone within the organization to invite guests and non-admins to the tenant, posing a security risk.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure", + "Remediation": { + "Code": { + "CLI": "Update-MgPolicyAuthorizationPolicy -AllowInvitesFrom 'adminsAndGuestInviters'", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Expand Identity > External Identities and select External collaboration settings. 3. Under Guest invite settings, set 'Guest invite restrictions' to 'Only users assigned to specific admin roles can invite guest users'. 4. Click Save.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict guest user invitations to only designated administrators or the Guest Inviter role to enhance security.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#guest-inviter" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "A more restrictive setting is acceptable, but the minimum requirement is limiting invitations to admins and Guest Inviters." +} diff --git a/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.py b/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.py new file mode 100644 index 0000000000..6dfb7b5e36 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.py @@ -0,0 +1,51 @@ +from typing import List + +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 InvitationsFrom + + +class entra_policy_guest_invite_only_for_admin_roles(Check): + """Check if guest invitations are restricted to users with specific administrative roles. + + This check verifies the `guest_invite_settings` property of the authorization policy. + If the setting is set to either "adminsAndGuestInviters" or "none", guest invitations + are limited accordingly. Otherwise, they are not restricted. + """ + + def execute(self) -> List[CheckReportMicrosoft365]: + """ + Execute the guest invitation restriction check. + + Retrieves the authorization policy from the Microsoft Entra client and checks + whether the 'guest_invite_settings' is set to restrict invitations to users with + specific administrative roles only. + + Returns: + List[CheckReportMicrosoft365]: A list containing a single check report that + details the pass/fail status and description. + """ + findings = [] + auth_policy = entra_client.authorization_policy + + report = CheckReportMicrosoft365( + metadata=self.metadata(), + resource=auth_policy if auth_policy else {}, + resource_name=auth_policy.name if auth_policy else "Authorization Policy", + resource_id=auth_policy.id if auth_policy else "authorizationPolicy", + ) + report.status = "FAIL" + report.status_extended = "Guest invitations are not restricted to users with specific administrative roles only." + + if ( + getattr(auth_policy, "guest_invite_settings", None) + == InvitationsFrom.ADMINS_AND_GUEST_INVITERS.value + ) or ( + getattr(auth_policy, "guest_invite_settings", None) + == InvitationsFrom.NONE.value + ): + report.status = "PASS" + report.status_extended = "Guest invitations are restricted to users with specific administrative roles only." + + 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 4acbefa940..6a88aad895 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -84,6 +84,7 @@ class Entra(Microsoft365Service): ) ], ), + guest_invite_settings=auth_policy.allow_invites_from, guest_user_role_id=auth_policy.guest_user_role_id, ) except Exception as error: @@ -441,6 +442,7 @@ class AuthorizationPolicy(BaseModel): name: str description: str default_user_role_permissions: Optional[DefaultUserRolePermissions] + guest_invite_settings: Optional[str] guest_user_role_id: Optional[UUID] @@ -482,6 +484,13 @@ class AdminRoles(Enum): USER_ADMINISTRATOR = "fe930be7-5e62-47db-91af-98c3a49a38b1" +class InvitationsFrom(Enum): + NONE = "none" + ADMINS_AND_GUEST_INVITERS = "adminsAndGuestInviters" + ADMINS_AND_GUEST_INVITERS_AND_MEMBERS = "adminsAndGuestInvitersAndAllMembers" + EVERYONE = "everyone" + + class AuthPolicyRoles(Enum): USER = UUID("a0b1b346-4d3e-4e8b-98f8-753987be4970") GUEST_USER = UUID("10dae51f-b6af-4016-8d66-8c2a99b929b3") diff --git a/tests/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/microsoft365_entra_policy_guest_invite_only_for_admin_roles_test.py b/tests/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/microsoft365_entra_policy_guest_invite_only_for_admin_roles_test.py new file mode 100644 index 0000000000..27eec2d350 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_policy_guest_invite_only_for_admin_roles/microsoft365_entra_policy_guest_invite_only_for_admin_roles_test.py @@ -0,0 +1,168 @@ +import mock + +from prowler.providers.microsoft365.services.entra.entra_service import ( + AuthorizationPolicy, + InvitationsFrom, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + set_mocked_microsoft365_provider, +) + + +class Test_entra_policy_guest_invite_only_for_admin_roles: + def test_no_auth_policy(self): + """ + Test when there is no authorization policy (auth_policy is None): + The check should return a report with FAIL status using default resource values. + """ + entra_client = mock.MagicMock() + entra_client.authorization_policy = None + + 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_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles import ( + entra_policy_guest_invite_only_for_admin_roles, + ) + + check = entra_policy_guest_invite_only_for_admin_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + "Guest invitations are not restricted to users with specific administrative roles only." + ) + assert result[0].resource_name == "Authorization Policy" + assert result[0].resource_id == "authorizationPolicy" + assert result[0].resource == {} + + def test_auth_policy_fail(self): + """ + Test when an authorization policy exists but guest_invite_settings is not set to a restricted value: + The check should FAIL. + """ + entra_client = mock.MagicMock() + + 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_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles import ( + entra_policy_guest_invite_only_for_admin_roles, + ) + + entra_client.authorization_policy = AuthorizationPolicy( + id="policy001", + name="Auth Policy Test", + description="Test policy", + guest_invite_settings=InvitationsFrom.EVERYONE.value, + ) + + check = entra_policy_guest_invite_only_for_admin_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + "Guest invitations are not restricted to users with specific administrative roles only." + ) + assert result[0].resource_id == "policy001" + assert result[0].resource_name == "Auth Policy Test" + assert result[0].location == "global" + assert result[0].resource == entra_client.authorization_policy.dict() + + def test_auth_policy_pass_admins_and_guest_inviters(self): + """ + Test when the authorization policy exists and guest_invite_settings is set to + InvitationsFrom.ADMINS_AND_GUEST_INVITERS: the check should PASS. + """ + entra_client = mock.MagicMock() + + 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_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles import ( + entra_policy_guest_invite_only_for_admin_roles, + ) + + entra_client.authorization_policy = AuthorizationPolicy( + id="policy002", + name="Auth Policy Restricted", + description="Test policy", + guest_invite_settings=InvitationsFrom.ADMINS_AND_GUEST_INVITERS.value, + ) + + check = entra_policy_guest_invite_only_for_admin_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + "Guest invitations are restricted to users with specific administrative roles only." + ) + assert result[0].resource_id == "policy002" + assert result[0].resource_name == "Auth Policy Restricted" + assert result[0].location == "global" + assert result[0].resource == entra_client.authorization_policy.dict() + + def test_auth_policy_pass_none(self): + """ + Test when the authorization policy exists and guest_invite_settings is set to + InvitationsFrom.NONE: the check should PASS. + """ + entra_client = mock.MagicMock() + + 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_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_policy_guest_invite_only_for_admin_roles.entra_policy_guest_invite_only_for_admin_roles import ( + entra_policy_guest_invite_only_for_admin_roles, + ) + + entra_client.authorization_policy = AuthorizationPolicy( + id="policy003", + name="Auth Policy Restricted None", + description="Test policy", + guest_invite_settings=InvitationsFrom.NONE.value, + ) + + check = entra_policy_guest_invite_only_for_admin_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + "Guest invitations are restricted to users with specific administrative roles only." + ) + assert result[0].resource_id == "policy003" + assert result[0].resource_name == "Auth Policy Restricted None" + assert result[0].location == "global" + assert result[0].resource == entra_client.authorization_policy.dict() 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 604d7e9cf6..df87759903 100644 --- a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py @@ -14,6 +14,7 @@ from prowler.providers.microsoft365.services.entra.entra_service import ( Entra, GrantControlOperator, GrantControls, + InvitationsFrom, Organization, PersistentBrowser, SessionControls, @@ -41,6 +42,7 @@ async def mock_entra_get_authorization_policy(_): allowed_to_read_bitlocker_keys_for_owned_device=True, allowed_to_read_other_users=True, ), + guest_invite_settings=InvitationsFrom.ADMINS_AND_GUEST_INVITERS.value, guest_user_role_id=AuthPolicyRoles.GUEST_USER_ACCESS_RESTRICTED.value, ) @@ -149,6 +151,10 @@ class Test_Entra_Service: allowed_to_read_other_users=True, ) ) + assert ( + entra_client.authorization_policy.guest_invite_settings + == InvitationsFrom.ADMINS_AND_GUEST_INVITERS.value + ) assert ( entra_client.authorization_policy.guest_user_role_id == AuthPolicyRoles.GUEST_USER_ACCESS_RESTRICTED.value