mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-09 11:17:08 +00:00
Compare commits
1 Commits
PROWLER-12
...
feat/prowl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0f109aacb |
@@ -207,6 +207,7 @@
|
||||
"entra_admin_portals_access_restriction",
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
|
||||
"entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"entra_seamless_sso_disabled"
|
||||
]
|
||||
@@ -685,6 +686,7 @@
|
||||
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
|
||||
"entra_conditional_access_policy_app_enforced_restrictions",
|
||||
"entra_conditional_access_policy_block_elevated_insider_risk",
|
||||
"entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"sharepoint_external_sharing_restricted"
|
||||
@@ -711,6 +713,7 @@
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
|
||||
"entra_conditional_access_policy_device_code_flow_blocked",
|
||||
"entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"entra_identity_protection_sign_in_risk_enabled",
|
||||
"entra_managed_device_required_for_authentication",
|
||||
"entra_seamless_sso_disabled",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"CheckTitle": "Conditional Access policy excludes Directory Synchronization Accounts to protect Entra Connect sync",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "Conditional Access policies scoped to **all users** and **all cloud applications** are evaluated to confirm the **Directory Synchronization Accounts** role is explicitly excluded. The Microsoft Entra Connect Sync Account does not support multifactor authentication, so it must be excluded from restrictive policies to maintain directory synchronization.",
|
||||
"Risk": "If the Directory Synchronization Accounts role is not excluded from Conditional Access policies requiring MFA or blocking access, the Entra Connect Sync Account will be unable to authenticate. This breaks hybrid identity synchronization between on-premises Active Directory and Entra ID, potentially causing authentication failures and identity inconsistencies.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-all-users-mfa",
|
||||
"https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/reference-connect-accounts-permissions",
|
||||
"https://maester.dev/docs/tests/MT.1020"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Open each policy that targets **All users** and **All cloud apps**.\n4. Under **Users** > **Exclude**, select **Directory roles** and add the **Directory Synchronization Accounts** role.\n5. Save the policy.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Exclude the Directory Synchronization Accounts role from all Conditional Access policies that target all users and all cloud applications. This prevents breaking Entra Connect directory synchronization while maintaining security controls for interactive users.",
|
||||
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_directory_sync_account_excluded"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access",
|
||||
"e3"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api"
|
||||
],
|
||||
"Notes": "This check corresponds to Maester test MT.1020 (Test-MtCaExclusionForDirectorySyncAccount). The Directory Synchronization Accounts role template ID is d29b2b05-8046-44ba-8758-1e26182fcf32."
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Check if Conditional Access policies exclude the Directory Synchronization Account."""
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicyState,
|
||||
)
|
||||
|
||||
# The Directory Synchronization Accounts built-in role template ID in Entra ID.
|
||||
# This role is assigned to the Microsoft Entra Connect Sync service account and
|
||||
# does not support multifactor authentication.
|
||||
DIRECTORY_SYNC_ROLE_TEMPLATE_ID = "d29b2b05-8046-44ba-8758-1e26182fcf32"
|
||||
|
||||
|
||||
class entra_conditional_access_policy_directory_sync_account_excluded(Check):
|
||||
"""Check that Conditional Access policies exclude the Directory Synchronization Account.
|
||||
|
||||
The Microsoft Entra Connect Sync Account cannot support MFA. Conditional
|
||||
Access policies scoped to all users and all cloud apps must explicitly
|
||||
exclude the Directory Synchronization Accounts role to prevent breaking
|
||||
directory synchronization.
|
||||
|
||||
- PASS: The policy excludes the Directory Synchronization Accounts role.
|
||||
- FAIL: The policy does not exclude the Directory Synchronization Accounts role.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the check for Directory Sync Account exclusion from Conditional Access policies.
|
||||
|
||||
Iterates through all enabled Conditional Access policies that target
|
||||
all users and all cloud applications, verifying each one excludes the
|
||||
Directory Synchronization Accounts role.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
for policy in entra_client.conditional_access_policies.values():
|
||||
if policy.state == ConditionalAccessPolicyState.DISABLED:
|
||||
continue
|
||||
|
||||
if not policy.conditions.user_conditions:
|
||||
continue
|
||||
|
||||
if "All" not in policy.conditions.user_conditions.included_users:
|
||||
continue
|
||||
|
||||
if not policy.conditions.application_conditions:
|
||||
continue
|
||||
|
||||
if (
|
||||
"All"
|
||||
not in policy.conditions.application_conditions.included_applications
|
||||
):
|
||||
continue
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.display_name,
|
||||
resource_id=policy.id,
|
||||
)
|
||||
|
||||
if (
|
||||
DIRECTORY_SYNC_ROLE_TEMPLATE_ID
|
||||
in policy.conditions.user_conditions.excluded_roles
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' excludes the Directory Synchronization Accounts role."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
if not findings:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Conditional Access Policies",
|
||||
resource_id="conditionalAccessPolicies",
|
||||
)
|
||||
report.status = "PASS"
|
||||
report.status_extended = "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -0,0 +1,711 @@
|
||||
"""Tests for the entra_conditional_access_policy_directory_sync_account_excluded check."""
|
||||
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ApplicationEnforcedRestrictions,
|
||||
ApplicationsConditions,
|
||||
ConditionalAccessGrantControl,
|
||||
ConditionalAccessPolicyState,
|
||||
Conditions,
|
||||
GrantControlOperator,
|
||||
GrantControls,
|
||||
PersistentBrowser,
|
||||
SessionControls,
|
||||
SignInFrequency,
|
||||
SignInFrequencyInterval,
|
||||
UsersConditions,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
DIRECTORY_SYNC_ROLE_TEMPLATE_ID = "d29b2b05-8046-44ba-8758-1e26182fcf32"
|
||||
|
||||
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded"
|
||||
|
||||
|
||||
def _default_session_controls():
|
||||
"""Return default session controls for test policies."""
|
||||
return SessionControls(
|
||||
persistent_browser=PersistentBrowser(is_enabled=False, mode="always"),
|
||||
sign_in_frequency=SignInFrequency(
|
||||
is_enabled=False,
|
||||
frequency=None,
|
||||
type=None,
|
||||
interval=SignInFrequencyInterval.EVERY_TIME,
|
||||
),
|
||||
application_enforced_restrictions=ApplicationEnforcedRestrictions(
|
||||
is_enabled=False
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _default_grant_controls():
|
||||
"""Return default grant controls requiring MFA for test policies."""
|
||||
return GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.MFA],
|
||||
operator=GrantControlOperator.AND,
|
||||
authentication_strength=None,
|
||||
)
|
||||
|
||||
|
||||
class Test_entra_conditional_access_policy_directory_sync_account_excluded:
|
||||
"""Test class for Directory Sync Account exclusion check."""
|
||||
|
||||
def test_no_conditional_access_policies(self):
|
||||
"""Test PASS when no Conditional Access policies exist."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
assert result[0].resource_id == "conditionalAccessPolicies"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_disabled(self):
|
||||
"""Test PASS when only a disabled policy exists targeting all users and apps."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Require MFA for All Users"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.DISABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
assert result[0].resource_id == "conditionalAccessPolicies"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_targets_specific_users(self):
|
||||
"""Test PASS when the policy targets specific users, not all users."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Require MFA for Admins"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=["some-group-id"],
|
||||
excluded_groups=[],
|
||||
included_users=[],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed."
|
||||
)
|
||||
|
||||
def test_policy_targets_specific_apps(self):
|
||||
"""Test PASS when the policy targets specific apps, not all apps."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Require MFA for Office 365"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["some-app-id"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed."
|
||||
)
|
||||
|
||||
def test_policy_enabled_without_sync_exclusion(self):
|
||||
"""Test FAIL when an enabled policy targets all users and all apps but does not exclude the sync role."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Require MFA for All Users"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.conditional_access_policies[policy_id].dict()
|
||||
)
|
||||
assert result[0].resource_name == display_name
|
||||
assert result[0].resource_id == policy_id
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_report_only_without_sync_exclusion(self):
|
||||
"""Test FAIL when a report-only policy targets all users and apps without excluding the sync role."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Report Only - Require MFA"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.conditional_access_policies[policy_id].dict()
|
||||
)
|
||||
assert result[0].resource_name == display_name
|
||||
assert result[0].resource_id == policy_id
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_enabled_with_sync_exclusion(self):
|
||||
"""Test PASS when an enabled policy targets all users and apps and excludes the sync role."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Require MFA for All Users"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[
|
||||
DIRECTORY_SYNC_ROLE_TEMPLATE_ID,
|
||||
],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' excludes the Directory Synchronization Accounts role."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.conditional_access_policies[policy_id].dict()
|
||||
)
|
||||
assert result[0].resource_name == display_name
|
||||
assert result[0].resource_id == policy_id
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_with_sync_role_and_other_excluded_roles(self):
|
||||
"""Test PASS when the sync role is excluded alongside other roles."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Require MFA for All Users"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[
|
||||
"some-other-role-id",
|
||||
DIRECTORY_SYNC_ROLE_TEMPLATE_ID,
|
||||
],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' excludes the Directory Synchronization Accounts role."
|
||||
)
|
||||
|
||||
def test_multiple_policies_mixed_results(self):
|
||||
"""Test multiple policies where one excludes sync role and another does not."""
|
||||
policy_id_pass = str(uuid4())
|
||||
policy_id_fail = str(uuid4())
|
||||
display_name_pass = "MFA Policy - With Exclusion"
|
||||
display_name_fail = "MFA Policy - Without Exclusion"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id_pass: ConditionalAccessPolicy(
|
||||
id=policy_id_pass,
|
||||
display_name=display_name_pass,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[
|
||||
DIRECTORY_SYNC_ROLE_TEMPLATE_ID,
|
||||
],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
),
|
||||
policy_id_fail: ConditionalAccessPolicy(
|
||||
id=policy_id_fail,
|
||||
display_name=display_name_fail,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
pass_results = [r for r in result if r.status == "PASS"]
|
||||
fail_results = [r for r in result if r.status == "FAIL"]
|
||||
|
||||
assert len(pass_results) == 1
|
||||
assert len(fail_results) == 1
|
||||
|
||||
assert pass_results[0].resource_name == display_name_pass
|
||||
assert pass_results[0].resource_id == policy_id_pass
|
||||
assert (
|
||||
pass_results[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name_pass}' excludes the Directory Synchronization Accounts role."
|
||||
)
|
||||
|
||||
assert fail_results[0].resource_name == display_name_fail
|
||||
assert fail_results[0].resource_id == policy_id_fail
|
||||
assert (
|
||||
fail_results[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name_fail}' does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync."
|
||||
)
|
||||
|
||||
def test_policy_with_wrong_excluded_role(self):
|
||||
"""Test FAIL when the policy excludes a different role but not the sync role."""
|
||||
policy_id = str(uuid4())
|
||||
display_name = "Require MFA for All Users"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import (
|
||||
entra_conditional_access_policy_directory_sync_account_excluded,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: ConditionalAccessPolicy(
|
||||
id=policy_id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=["some-other-role-id"],
|
||||
),
|
||||
client_app_types=[],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=_default_grant_controls(),
|
||||
session_controls=_default_session_controls(),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_conditional_access_policy_directory_sync_account_excluded()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync."
|
||||
)
|
||||
assert result[0].resource_name == display_name
|
||||
assert result[0].resource_id == policy_id
|
||||
Reference in New Issue
Block a user