diff --git a/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/__init__.py b/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.metadata.json b/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.metadata.json new file mode 100644 index 0000000000..9966b06066 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "entra_password_hash_sync_enabled", + "CheckTitle": "Ensure that password hash sync is enabled for hybrid deployments.", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Organization Settings", + "Description": "Ensure that password hash synchronization is enabled in hybrid Microsoft Entra deployments to facilitate seamless authentication and leaked credential protection.", + "Risk": "If password hash synchronization is not enabled, users may need to maintain multiple passwords, increasing security risks. Additionally, leaked credential detection for hybrid accounts would not be available, reducing the organization's ability to prevent account compromises.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-phs", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Log in to the on-premises server hosting Microsoft Entra Connect. 2. Open Azure AD Connect and click Configure. 3. Select 'Customize synchronization options' and click Next. 4. Provide admin credentials. 5. On the Optional features screen, check 'Password hash synchronization' and click Next. 6. Click Configure and wait for the process to complete.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable password hash synchronization in Microsoft Entra Connect to streamline authentication and enhance security monitoring.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-phs" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Applies only to hybrid Microsoft Entra deployments using Entra Connect sync and does not apply to federated domains." +} diff --git a/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.py b/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.py new file mode 100644 index 0000000000..57d4868fc3 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.py @@ -0,0 +1,45 @@ +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_password_hash_sync_enabled(Check): + """ + Check if password hash synchronization is enabled for hybrid Microsoft Entra deployments. + + This check verifies that password hash synchronization is enabled in hybrid Microsoft Entra deployments. + Enabling password hash sync ensures that on-premises passwords are synchronized to Microsoft Entra, + facilitating seamless authentication and enhancing leaked credential protection. Without password hash + synchronization, users might have to manage multiple passwords and detection of leaked credentials would be compromised. + + Note: This control applies only to hybrid deployments using Microsoft Entra Connect sync and does not apply to federated domains. + """ + + def execute(self) -> List[CheckReportMicrosoft365]: + """ + Execute the password hash synchronization requirement check. + + Retrieves the organization settings from the Entra client and generates a report indicating whether + password hash synchronization is enabled. + + Returns: + List[CheckReportMicrosoft365]: A list containing the report object with the result of the check. + """ + findings = [] + for organization in entra_client.organizations: + report = CheckReportMicrosoft365( + self.metadata(), + resource=organization, + resource_id=organization.id, + resource_name=organization.name, + ) + report.status = "FAIL" + report.status_extended = "Password hash synchronization is not enabled for hybrid Microsoft Entra deployments." + + if organization.on_premises_sync_enabled: + report.status = "PASS" + report.status_extended = "Password hash synchronization is enabled for hybrid Microsoft Entra deployments." + + 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 62ddba0e87..e3a30f2a13 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -21,6 +21,7 @@ class Entra(Microsoft365Service): self._get_conditional_access_policies(), self._get_admin_consent_policy(), self._get_groups(), + self._get_organization(), ) ) @@ -28,6 +29,7 @@ class Entra(Microsoft365Service): self.conditional_access_policies = attributes[1] self.admin_consent_policy = attributes[2] self.groups = attributes[3] + self.organizations = attributes[4] async def _get_authorization_policy(self): logger.info("Entra - Getting authorization policy...") @@ -238,6 +240,31 @@ 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 @@ -363,6 +390,12 @@ class AuthorizationPolicy(BaseModel): default_user_role_permissions: Optional[DefaultUserRolePermissions] +class Organization(BaseModel): + id: str + name: str + on_premises_sync_enabled: bool + + class Group(BaseModel): id: str name: str diff --git a/tests/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled_test.py new file mode 100644 index 0000000000..b363d4933d --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled_test.py @@ -0,0 +1,147 @@ +from unittest import mock + +from prowler.providers.microsoft365.services.entra.entra_service import Organization +from tests.providers.microsoft365.microsoft365_fixtures import ( + set_mocked_microsoft365_provider, +) + + +class Test_entra_password_hash_sync_enabled: + def test_password_hash_sync_enabled(self): + 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_password_hash_sync_enabled.entra_password_hash_sync_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_password_hash_sync_enabled.entra_password_hash_sync_enabled import ( + entra_password_hash_sync_enabled, + ) + + org = Organization( + id="org1", + name="Organization 1", + on_premises_sync_enabled=True, + ) + entra_client.organizations = [org] + + check = entra_password_hash_sync_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Password hash synchronization is enabled for hybrid Microsoft Entra deployments." + ) + assert result[0].resource_id == "org1" + assert result[0].resource_name == "Organization 1" + assert result[0].location == "global" + assert result[0].resource == org.dict() + + def test_password_hash_sync_disabled(self): + 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_password_hash_sync_enabled.entra_password_hash_sync_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_password_hash_sync_enabled.entra_password_hash_sync_enabled import ( + entra_password_hash_sync_enabled, + ) + + org1 = Organization( + id="org1", + name="Organization 1", + on_premises_sync_enabled=False, + ) + org2 = Organization( + id="org2", + name="Organization 2", + on_premises_sync_enabled=True, + ) + entra_client.organizations = [org1, org2] + + check = entra_password_hash_sync_enabled() + result = check.execute() + + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Password hash synchronization is not enabled for hybrid Microsoft Entra deployments." + ) + assert result[0].resource_id == "org1" + assert result[0].resource_name == "Organization 1" + assert result[0].location == "global" + assert result[0].resource == org1.dict() + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Password hash synchronization is enabled for hybrid Microsoft Entra deployments." + ) + assert result[1].resource_id == "org2" + assert result[1].resource_name == "Organization 2" + assert result[1].location == "global" + assert result[1].resource == org2.dict() + + def test_password_hash_sync_disabled_two_org(self): + 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_password_hash_sync_enabled.entra_password_hash_sync_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_password_hash_sync_enabled.entra_password_hash_sync_enabled import ( + entra_password_hash_sync_enabled, + ) + + org = Organization( + id="org2", + name="Organization 2", + on_premises_sync_enabled=False, + ) + entra_client.organizations = [org] + + check = entra_password_hash_sync_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Password hash synchronization is not enabled for hybrid Microsoft Entra deployments." + ) + assert result[0].resource_id == "org2" + assert result[0].resource_name == "Organization 2" + assert result[0].location == "global" + assert result[0].resource == org.dict() + + def test_empty_organization(self): + entra_client = mock.MagicMock() + entra_client.organization = [] + + 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_password_hash_sync_enabled.entra_password_hash_sync_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_password_hash_sync_enabled.entra_password_hash_sync_enabled import ( + entra_password_hash_sync_enabled, + ) + + check = entra_password_hash_sync_enabled() + result = check.execute() + + assert len(result) == 0 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 3162bac3b9..6e507d7372 100644 --- a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py @@ -12,6 +12,7 @@ from prowler.providers.microsoft365.services.entra.entra_service import ( DefaultUserRolePermissions, Entra, GrantControls, + Organization, PersistentBrowser, SessionControls, SignInFrequency, @@ -40,6 +41,16 @@ 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( @@ -202,3 +213,14 @@ class Test_Entra_Service: assert entra_client.admin_consent_policy.notify_reviewers assert entra_client.admin_consent_policy.email_reminders_to_reviewers is False assert entra_client.admin_consent_policy.duration_in_days == 30 + + @patch( + "prowler.providers.microsoft365.services.entra.entra_service.Entra._get_organization", + new=mock_entra_get_organization, + ) + def test_get_organization(self): + entra_client = Entra(set_mocked_microsoft365_provider()) + assert len(entra_client.organizations) == 1 + assert entra_client.organizations[0].id == "org1" + assert entra_client.organizations[0].name == "Organization 1" + assert entra_client.organizations[0].on_premises_sync_enabled