feat(entra): add new check entra_dynamic_group_for_guests_created (#7168)

Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
Daniel Barranquero
2025-03-11 16:21:17 +01:00
committed by GitHub
parent 66df5f7a1c
commit ed17931117
6 changed files with 256 additions and 1 deletions
@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "entra_dynamic_group_for_guests_created",
"CheckTitle": "Ensure a dynamic group for guest users is created.",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Group Settings",
"Description": "Ensure that a dynamic group is created for guest users in Microsoft Entra to enforce conditional access policies and security controls automatically.",
"Risk": "Without a dynamic group for guest users, administrators may need to manually manage access controls, leading to potential security gaps and inconsistent policy enforcement.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/users/groups-create-rule",
"Remediation": {
"Code": {
"CLI": "New-MgGroup -DisplayName 'Dynamic Guest Users' -MailNickname 'DynGuestUsers' -MailEnabled $false -SecurityEnabled $true -GroupTypes 'DynamicMembership' -MembershipRule '(user.userType -eq \"Guest\")' -MembershipRuleProcessingState 'On'",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity > Groups and select All groups. 3. Select 'New group' and configure: Group type: Security, Membership type: Dynamic User. 4. Add dynamic query with rule: (user.userType -eq 'Guest'). 5. Click Save.",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a dynamic group for guest users to automate policy enforcement and access control.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/users/groups-create-rule"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,54 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportMicrosoft365
from prowler.providers.microsoft365.services.entra.entra_client import entra_client
class entra_dynamic_group_for_guests_created(Check):
"""
Check if a dynamic group for guest users is created in Microsoft Entra.
This check verifies that a dynamic group exists for guest users in Microsoft Entra.
A dynamic group for guest users should have the group type 'DynamicMembership' and a membership rule
that restricts membership to users with a userType equal to 'Guest'. This configuration enables
automated enforcement of conditional access policies and reduces manual management of guest access.
"""
def execute(self) -> List[CheckReportMicrosoft365]:
"""
Execute the dynamic group for guest users check.
Iterates over the groups retrieved from the Microsoft Entra client and generates a report
indicating whether at least one dynamic group exists with a membership rule targeting guest users.
Returns:
List[CheckReportMicrosoft365]: A list containing a single report with the result of the check.
"""
findings = []
if entra_client.groups:
dynamic_group = None
for group in entra_client.groups:
if "DynamicMembership" in group.groupTypes and group.membershipRule:
if 'user.userType -eq "Guest"' in group.membershipRule:
dynamic_group = group
break
report = CheckReportMicrosoft365(
self.metadata(),
resource=dynamic_group if dynamic_group else {},
resource_name=dynamic_group.name if dynamic_group else "Group",
resource_id=dynamic_group.id if dynamic_group else "group",
)
report.status = "FAIL"
report.status_extended = (
"No dynamic group for guest users was found in Microsoft Entra."
)
if dynamic_group:
report.status = "PASS"
report.status_extended = (
"A dynamic group for guest users is created in Microsoft Entra."
)
findings.append(report)
return findings
@@ -17,12 +17,14 @@ class Entra(Microsoft365Service):
attributes = loop.run_until_complete(
gather(
self._get_authorization_policy(),
self._get_groups(),
self._get_admin_consent_policy(),
)
)
self.authorization_policy = attributes[0]
self.admin_consent_policy = attributes[1]
self.groups = attributes[1]
self.admin_consent_policy = attributes[2]
async def _get_authorization_policy(self):
logger.info("Entra - Getting authorization policy...")
@@ -85,6 +87,26 @@ class Entra(Microsoft365Service):
return authorization_policy
async def _get_groups(self):
logger.info("Entra - Getting groups...")
groups = []
try:
groups_data = await self.client.groups.get()
for group in groups_data.value:
groups.append(
Group(
id=group.id,
name=group.display_name,
groupTypes=group.group_types,
membershipRule=group.membership_rule,
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return groups
async def _get_admin_consent_policy(self):
logger.info("Entra - Getting group settings...")
admin_consent_policy = None
@@ -121,6 +143,13 @@ class AuthorizationPolicy(BaseModel):
default_user_role_permissions: Optional[DefaultUserRolePermissions]
class Group(BaseModel):
id: str
name: str
groupTypes: List[str]
membershipRule: Optional[str]
class AdminConsentPolicy(BaseModel):
admin_consent_enabled: bool
notify_reviewers: bool
@@ -0,0 +1,110 @@
from unittest import mock
from prowler.providers.microsoft365.services.entra.entra_service import Group
from tests.providers.microsoft365.microsoft365_fixtures import (
set_mocked_microsoft365_provider,
)
class Test_entra_dynamic_group_for_guests_created:
def test_no_groups(self):
"""
Test when no groups exist:
The check should return an empty list of findings.
"""
entra_client = mock.MagicMock()
entra_client.groups = []
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_dynamic_group_for_guests_created.entra_dynamic_group_for_guests_created.entra_client",
new=entra_client,
):
from prowler.providers.microsoft365.services.entra.entra_dynamic_group_for_guests_created.entra_dynamic_group_for_guests_created import (
entra_dynamic_group_for_guests_created,
)
check = entra_dynamic_group_for_guests_created()
result = check.execute()
assert len(result) == 0
def test_group_not_dynamic(self):
"""
Test when a group exists but is not dynamic:
The check should FAIL with the default message.
"""
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_dynamic_group_for_guests_created.entra_dynamic_group_for_guests_created.entra_client",
new=entra_client,
):
entra_client.groups = [
Group(
id="group1",
name="Group 1",
groupTypes=["Unified"],
membershipRule=None,
)
]
from prowler.providers.microsoft365.services.entra.entra_dynamic_group_for_guests_created.entra_dynamic_group_for_guests_created import (
entra_dynamic_group_for_guests_created,
)
check = entra_dynamic_group_for_guests_created()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == (
"No dynamic group for guest users was found in Microsoft Entra."
)
assert result[0].resource_id == "group"
assert result[0].resource_name == "Group"
assert result[0].location == "global"
assert result[0].resource == {}
def test_dynamic_group_with_proper_membership_rule(self):
"""
Test when a group is dynamic and its membership rule correctly restricts guest users:
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_dynamic_group_for_guests_created.entra_dynamic_group_for_guests_created.entra_client",
new=entra_client,
):
entra_client.groups = [
Group(
id="group3",
name="Group 3",
groupTypes=["DynamicMembership"],
membershipRule='user.userType -eq "Guest"',
)
]
from prowler.providers.microsoft365.services.entra.entra_dynamic_group_for_guests_created.entra_dynamic_group_for_guests_created import (
entra_dynamic_group_for_guests_created,
)
check = entra_dynamic_group_for_guests_created()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "A dynamic group for guest users is created in Microsoft Entra."
)
assert result[0].resource_id == "group3"
assert result[0].resource_name == "Group 3"
assert result[0].location == "global"
assert result[0].resource == entra_client.groups[0].dict()
@@ -28,6 +28,22 @@ async def mock_entra_get_authorization_policy(_):
)
async def mock_entra_get_groups(_):
group1 = {
"id": "id-1",
"name": "group1",
"groupTypes": ["DynamicMembership"],
"membershipRule": 'user.userType -eq "Guest"',
}
group2 = {
"id": "id-2",
"name": "group2",
"groupTypes": ["Assigned"],
"membershipRule": "",
}
return [group1, group2]
async def mock_entra_get_admin_consent_policy(_):
return AdminConsentPolicy(
admin_consent_enabled=True,
@@ -66,6 +82,22 @@ class Test_Entra_Service:
)
)
@patch(
"prowler.providers.microsoft365.services.entra.entra_service.Entra._get_groups",
new=mock_entra_get_groups,
)
def test_get_groups(self):
entra_client = Entra(set_mocked_microsoft365_provider())
assert len(entra_client.groups) == 2
assert entra_client.groups[0]["id"] == "id-1"
assert entra_client.groups[0]["name"] == "group1"
assert entra_client.groups[0]["groupTypes"] == ["DynamicMembership"]
assert entra_client.groups[0]["membershipRule"] == 'user.userType -eq "Guest"'
assert entra_client.groups[1]["id"] == "id-2"
assert entra_client.groups[1]["name"] == "group2"
assert entra_client.groups[1]["groupTypes"] == ["Assigned"]
assert entra_client.groups[1]["membershipRule"] == ""
@patch(
"prowler.providers.microsoft365.services.entra.entra_service.Entra._get_admin_consent_policy",
new=mock_entra_get_admin_consent_policy,