fix(sdk): tighten entra sign-in frequency filter matching

- Validate device filter semantics by mode

- Prevent corporate-device false positives

- Add regression tests for include and exclude rules
This commit is contained in:
Hugo P.Brito committed 2026-04-09 13:28:22 +01:00
1 parent bba1ef4ee4
commit 76429871b7
2 files changed
+96 -10

No files matched your search

@@ -1,3 +1,5 @@
import re
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 (
@@ -20,6 +22,18 @@ class entra_conditional_access_policy_sign_in_frequency_enforced(Check):
non-corporate devices.
"""
NON_CORPORATE_INCLUDE_PATTERNS = (
r"device\.iscompliant\s*-ne\s*true",
r"device\.iscompliant\s*-eq\s*false",
r'device\.trusttype\s*-ne\s*"serverad"',
r"device\.trusttype\s*-ne\s*'serverad'",
)
CORPORATE_EXCLUDE_PATTERNS = (
r"device\.iscompliant\s*-eq\s*true",
r'device\.trusttype\s*-eq\s*"serverad"',
r"device\.trusttype\s*-eq\s*'serverad'",
)
def _targets_all_users(self, included_users: list[str]) -> bool:
"""Check if the policy targets all users.
@@ -61,18 +75,16 @@ class entra_conditional_access_policy_sign_in_frequency_enforced(Check):
return False
if not device_conditions.device_filter_rule:
return False
rule = device_conditions.device_filter_rule.lower()
if device_conditions.device_filter_mode == DeviceFilterMode.INCLUDE:
rule = device_conditions.device_filter_rule.lower()
return (
"device.iscompliant" in rule or "device.trusttype" in rule
)
return self._rule_matches_any(rule, self.NON_CORPORATE_INCLUDE_PATTERNS)
elif device_conditions.device_filter_mode == DeviceFilterMode.EXCLUDE:
rule = device_conditions.device_filter_rule.lower()
return (
"device.iscompliant" in rule or "device.trusttype" in rule
)
return self._rule_matches_any(rule, self.CORPORATE_EXCLUDE_PATTERNS)
return False
def _rule_matches_any(self, rule: str, patterns: tuple[str, ...]) -> bool:
return any(re.search(pattern, rule) for pattern in patterns)
def execute(self) -> list[CheckReportM365]:
"""Execute the check for sign-in frequency enforcement in Conditional Access policies.
@@ -147,7 +147,7 @@ class Test_entra_conditional_access_policy_sign_in_frequency_enforced:
display_name="Disabled Policy",
state=ConditionalAccessPolicyState.DISABLED,
device_filter_mode=DeviceFilterMode.INCLUDE,
device_filter_rule="device.isCompliant -ne True -or device.trustType -ne \"ServerAD\"",
device_filter_rule='device.isCompliant -ne True -or device.trustType -ne "ServerAD"',
)
}
@@ -187,7 +187,7 @@ class Test_entra_conditional_access_policy_sign_in_frequency_enforced:
display_name=display_name,
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
device_filter_mode=DeviceFilterMode.INCLUDE,
device_filter_rule="device.isCompliant -ne True -or device.trustType -ne \"ServerAD\"",
device_filter_rule='device.isCompliant -ne True -or device.trustType -ne "ServerAD"',
)
}
@@ -509,6 +509,80 @@ class Test_entra_conditional_access_policy_sign_in_frequency_enforced:
assert result[0].status == "FAIL"
assert result[0].resource_name == "Conditional Access Policies"
def test_entra_policy_device_filter_include_corporate_devices(self):
"""Test FAIL when include mode targets only corporate devices."""
policy_id = str(uuid4())
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_sign_in_frequency_enforced.entra_conditional_access_policy_sign_in_frequency_enforced import (
entra_conditional_access_policy_sign_in_frequency_enforced,
)
entra_client.conditional_access_policies = {
policy_id: _make_policy(
policy_id=policy_id,
display_name="Corporate Devices Policy",
device_filter_mode=DeviceFilterMode.INCLUDE,
device_filter_rule="device.isCompliant -eq True",
)
}
check = entra_conditional_access_policy_sign_in_frequency_enforced()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "Conditional Access Policies"
def test_entra_policy_device_filter_exclude_non_corporate_devices(self):
"""Test FAIL when exclude mode excludes non-corporate devices."""
policy_id = str(uuid4())
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_sign_in_frequency_enforced.entra_conditional_access_policy_sign_in_frequency_enforced import (
entra_conditional_access_policy_sign_in_frequency_enforced,
)
entra_client.conditional_access_policies = {
policy_id: _make_policy(
policy_id=policy_id,
display_name="Exclude Unmanaged Devices Policy",
device_filter_mode=DeviceFilterMode.EXCLUDE,
device_filter_rule="device.isCompliant -eq False",
)
}
check = entra_conditional_access_policy_sign_in_frequency_enforced()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "Conditional Access Policies"
def test_entra_multiple_policies_one_compliant(self):
"""Test PASS when at least one policy among multiple is compliant."""
policy_id_1 = str(uuid4())