diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index b43f379f5e..b63975c384 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed - Alibaba Cloud CS service SDK compatibility, harden other services and improve documentation [(#10871)](https://github.com/prowler-cloud/prowler/pull/10871) +- `admincenter_groups_not_public_visibility` check for M365 provider evaluating Security and Distribution groups, now restricted to Microsoft 365 (Unified) groups per CIS M365 Foundations 1.2.1 [(#10899)](https://github.com/prowler-cloud/prowler/pull/10899) --- diff --git a/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py b/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py index f38f94f64f..fd054753c1 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py +++ b/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py @@ -27,6 +27,11 @@ class admincenter_groups_not_public_visibility(Check): """ findings = [] for group in admincenter_client.groups.values(): + # Only Microsoft 365 Groups (identified by the "Unified" group type) are in + # scope for this check per CIS M365 Foundations 1.2.1. Security, + # Distribution, and other group types are skipped. + if "Unified" not in group.group_types: + continue report = CheckReportM365( metadata=self.metadata(), resource=group, diff --git a/prowler/providers/m365/services/admincenter/admincenter_service.py b/prowler/providers/m365/services/admincenter/admincenter_service.py index fdf864aed9..45be16d11a 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_service.py +++ b/prowler/providers/m365/services/admincenter/admincenter_service.py @@ -182,6 +182,7 @@ class AdminCenter(M365Service): id=group.id, name=getattr(group, "display_name", ""), visibility=getattr(group, "visibility", ""), + group_types=getattr(group, "group_types", []) or [], ) } ) @@ -237,6 +238,7 @@ class Group(BaseModel): id: str name: str visibility: Optional[str] + group_types: List[str] = [] class PasswordPolicy(BaseModel): diff --git a/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py b/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py index 7391bd102e..8b414a170c 100644 --- a/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py +++ b/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py @@ -61,7 +61,12 @@ class Test_admincenter_groups_not_public_visibility: id_group1 = str(uuid4()) admincenter_client.groups = { - id_group1: Group(id=id_group1, name="Group1", visibility="Private"), + id_group1: Group( + id=id_group1, + name="Group1", + visibility="Private", + group_types=["Unified"], + ), } check = admincenter_groups_not_public_visibility() @@ -102,7 +107,12 @@ class Test_admincenter_groups_not_public_visibility: id_group1 = str(uuid4()) admincenter_client.groups = { - id_group1: Group(id=id_group1, name="Group1", visibility="Private"), + id_group1: Group( + id=id_group1, + name="Group1", + visibility="Private", + group_types=["Unified"], + ), } check = admincenter_groups_not_public_visibility() @@ -143,7 +153,12 @@ class Test_admincenter_groups_not_public_visibility: id_group1 = str(uuid4()) admincenter_client.groups = { - id_group1: Group(id=id_group1, name="Group1", visibility="Public"), + id_group1: Group( + id=id_group1, + name="Group1", + visibility="Public", + group_types=["Unified"], + ), } check = admincenter_groups_not_public_visibility() @@ -187,7 +202,12 @@ class Test_admincenter_groups_not_public_visibility: id_group1 = str(uuid4()) admincenter_client.groups = { - id_group1: Group(id=id_group1, name="Group1", visibility=None), + id_group1: Group( + id=id_group1, + name="Group1", + visibility=None, + group_types=["Unified"], + ), } check = admincenter_groups_not_public_visibility() @@ -202,3 +222,60 @@ class Test_admincenter_groups_not_public_visibility: assert result[0].resource_name == "Group1" assert result[0].resource_id == id_group1 assert result[0].location == "global" + + def test_admincenter_security_group_ignored(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + from prowler.providers.m365.services.admincenter.admincenter_service import ( + Group, + ) + + id_security_group = str(uuid4()) + id_distribution_group = str(uuid4()) + id_m365_group = str(uuid4()) + + admincenter_client.groups = { + id_security_group: Group( + id=id_security_group, + name="SecurityGroup", + visibility=None, + group_types=[], + ), + id_distribution_group: Group( + id=id_distribution_group, + name="DistributionGroup", + visibility=None, + group_types=[], + ), + id_m365_group: Group( + id=id_m365_group, + name="M365Group", + visibility="Private", + group_types=["Unified"], + ), + } + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == id_m365_group + assert result[0].status == "PASS" + assert result[0].resource_name == "M365Group" diff --git a/tests/providers/m365/services/admincenter/admincenter_service_test.py b/tests/providers/m365/services/admincenter/admincenter_service_test.py index 6286dcc978..ce0a2a3050 100644 --- a/tests/providers/m365/services/admincenter/admincenter_service_test.py +++ b/tests/providers/m365/services/admincenter/admincenter_service_test.py @@ -214,3 +214,41 @@ def test_admincenter__get_users_handles_pagination(): with_url_mock.assert_called_once_with("next-link") assert users["user-1"].license == "SKU-user-1" assert users["user-3"].license == "SKU-user-3" + + +def test_admincenter__get_groups_maps_group_types(): + admincenter_service = AdminCenter.__new__(AdminCenter) + + groups_response = SimpleNamespace( + value=[ + SimpleNamespace( + id="id-1", + display_name="Unified Group", + visibility="Private", + group_types=["Unified"], + ), + SimpleNamespace( + id="id-2", + display_name="Security Group", + visibility=None, + group_types=[], + ), + SimpleNamespace( + id="id-3", + display_name="Legacy Group", + visibility="Public", + ), + ] + ) + + groups_builder = SimpleNamespace(get=AsyncMock(return_value=groups_response)) + admincenter_service.client = SimpleNamespace(groups=groups_builder) + + groups = asyncio.run(admincenter_service._get_groups()) + + assert len(groups) == 3 + assert groups_builder.get.await_count == 1 + assert groups["id-1"].group_types == ["Unified"] + assert groups["id-2"].group_types == [] + assert groups["id-3"].group_types == [] + assert groups["id-3"].visibility == "Public"