feat(entra): add new check entra_password_hash_sync_enabled (#7061)

This commit is contained in:
Daniel Barranquero
2025-03-12 11:31:49 +01:00
committed by Pepe Fagoaga
parent 5fc95a879c
commit 35a9c03467
6 changed files with 277 additions and 0 deletions
@@ -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."
}
@@ -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
@@ -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
@@ -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
@@ -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