mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
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:
committed by
GitHub
parent
66df5f7a1c
commit
ed17931117
+30
@@ -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": ""
|
||||
}
|
||||
+54
@@ -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
|
||||
|
||||
+110
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user