Compare commits

...

1 Commits

Author SHA1 Message Date
HugoPBrito
6360df6e62 feat(m365): add entra_conditional_access_policy_require_password_change_high_risk_users security check
Add new security check entra_conditional_access_policy_require_password_change_high_risk_users for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-02-24 17:02:26 +01:00
10 changed files with 859 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `entra_conditional_access_policy_require_password_change_high_risk_users` check for m365 provider [(#10160)](https://github.com/prowler-cloud/prowler/pull/10160)
- `entra_app_registration_no_unused_privileged_permissions` check for m365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
- `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087)
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)

View File

@@ -1237,6 +1237,7 @@
"Id": "5.2.2.6",
"Description": "Microsoft Entra ID Protection user risk policies detect the probability that a user account has been compromised. **Note:** While Identity Protection also provides two risk policies with limited conditions, Microsoft highly recommends setting up risk-based policies in Conditional Access as opposed to the \"legacy method\" for the following benefits:- Enhanced diagnostic data- Report-only mode integration- Graph API support- Use more Conditional Access attributes like sign-in frequency in the policy",
"Checks": [
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_user_risk_enabled"
],
"Attributes": [

View File

@@ -1469,6 +1469,7 @@
"Id": "5.2.2.6",
"Description": "Microsoft Entra ID Protection user risk policies detect the probability that a user account has been compromised. Enable Identity Protection user risk policies.",
"Checks": [
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_user_risk_enabled"
],
"Attributes": [

View File

@@ -20,6 +20,7 @@
"Checks": [
"defender_antiphishing_policy_configured",
"defender_antispam_policy_inbound_no_allowed_domains",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]
@@ -120,6 +121,7 @@
"defenderxdr_endpoint_privileged_user_exposed_credentials",
"defender_identity_health_issues_no_open",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]
@@ -202,6 +204,7 @@
"admincenter_users_admins_reduced_license_footprint",
"entra_admin_portals_access_restriction",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_policy_guest_users_access_restrictions",
"entra_seamless_sso_disabled"
]
@@ -240,6 +243,7 @@
"entra_admin_users_sign_in_frequency_enabled",
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_legacy_authentication_blocked",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
@@ -284,6 +288,7 @@
],
"Checks": [
"sharepoint_external_sharing_managed",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]
@@ -302,6 +307,7 @@
],
"Checks": [
"sharepoint_external_sharing_managed",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]
@@ -432,6 +438,7 @@
],
"Checks": [
"sharepoint_external_sharing_restricted",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]
@@ -461,6 +468,7 @@
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled",
"entra_legacy_authentication_blocked",
@@ -685,6 +693,7 @@
"Checks": [
"entra_admin_users_sign_in_frequency_enabled",
"entra_admin_users_mfa_enabled",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
"entra_users_mfa_enabled",
@@ -895,6 +904,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]

View File

@@ -388,6 +388,7 @@
"Description": "Enable Identity Protection user risk policies",
"Checks": [
"defenderxdr_endpoint_privileged_user_exposed_credentials",
"entra_conditional_access_policy_require_password_change_high_risk_users",
"entra_identity_protection_user_risk_enabled"
],
"Attributes": [

View File

@@ -0,0 +1,39 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_require_password_change_high_risk_users",
"CheckTitle": "Conditional Access policy enforces password change and MFA for high-risk users",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "Conditional Access Policy",
"ResourceGroup": "IAM",
"Description": "This check verifies that at least one enabled Conditional Access policy requires both **password change** and **MFA** (with AND operator) for users flagged with a **high** user risk level. The policy must apply to all users and all cloud applications to ensure comprehensive coverage.",
"Risk": "Without a policy enforcing password change and MFA for high-risk users, compromised accounts may retain their stolen credentials, allowing attackers to maintain persistent access. High-risk users identified by Identity Protection remain vulnerable to credential theft, privilege escalation, and data exfiltration.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-risk-user"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center (https://entra.microsoft.com).\n2. Go to Protection > Conditional Access > Policies.\n3. Click 'New policy' to create a new Conditional Access policy.\n4. Under Users, select 'All users'.\n5. Under Target resources > Cloud apps, select 'All cloud apps'.\n6. Under Conditions > User risk, set to 'Yes' and select 'High'.\n7. Under Grant, click 'Grant access' and select both 'Require multifactor authentication' and 'Require password change', ensuring the operator is set to 'AND'.\n8. Set Enable policy to 'On'.\n9. Click 'Create'.",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that requires both MFA and password change for high-risk users. This ensures that when Identity Protection detects a compromised account, the user must re-authenticate with MFA and reset their password to regain access. Requires Entra ID P2 license. For hybrid users synced from on-premises AD, password writeback must be enabled.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_require_password_change_high_risk_users"
}
},
"Categories": [
"identity-access",
"e5"
],
"DependsOn": [],
"RelatedTo": [
"entra_identity_protection_user_risk_enabled"
],
"Notes": "Requires Entra ID P2 license. For hybrid users synced from on-premises Active Directory, password writeback must be enabled for this policy to function correctly."
}

View File

@@ -0,0 +1,89 @@
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 (
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
GrantControlOperator,
RiskLevel,
)
class entra_conditional_access_policy_require_password_change_high_risk_users(Check):
"""Check if a Conditional Access policy requires password change and MFA for high-risk users.
This check verifies that at least one enabled Conditional Access policy enforces
both password change and MFA (with AND operator) for users flagged with high user
risk level, covering all users and all cloud applications.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check for password change requirement on high-risk users.
Iterates through all Conditional Access policies to find one that:
- Is enabled (not disabled or report-only)
- Applies to all users and all cloud applications
- Includes 'high' in user risk levels
- Requires both 'mfa' and 'passwordChange' with AND operator
Returns:
list[CheckReportM365]: A list containing the check result.
"""
findings = []
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Conditional Access Policies",
resource_id="conditionalAccessPolicies",
)
report.status = "FAIL"
report.status_extended = "No Conditional Access Policy requires password change and MFA for high-risk users."
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if "All" not in policy.conditions.user_conditions.included_users:
continue
if (
"All"
not in policy.conditions.application_conditions.included_applications
):
continue
if (
ConditionalAccessGrantControl.PASSWORD_CHANGE
not in policy.grant_controls.built_in_controls
or ConditionalAccessGrantControl.MFA
not in policy.grant_controls.built_in_controls
or policy.grant_controls.operator != GrantControlOperator.AND
):
continue
if not policy.conditions.user_risk_levels:
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.display_name,
resource_id=policy.id,
)
if RiskLevel.HIGH not in policy.conditions.user_risk_levels:
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires password change and MFA for user risk but does not include 'high' risk level."
elif (
policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING
):
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires password change and MFA for high-risk users but is set to report-only mode and does not enforce it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires password change and MFA for high-risk users."
break
findings.append(report)
return findings

View File

@@ -0,0 +1,717 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
PersistentBrowser,
RiskLevel,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users"
class Test_entra_conditional_access_policy_require_password_change_high_risk_users:
def test_no_conditional_access_policies(self):
"""Test FAIL 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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
entra_client.conditional_access_policies = {}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires password change and MFA for high-risk users."
)
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 FAIL when the only matching policy is disabled."""
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name="Test",
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=[],
),
user_risk_levels=[RiskLevel.HIGH],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
ConditionalAccessGrantControl.PASSWORD_CHANGE,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires password change and MFA for high-risk users."
)
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_without_high_risk_level(self):
"""Test FAIL when policy has user risk levels but does not include HIGH."""
id = str(uuid4())
display_name = "Test"
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
),
user_risk_levels=[RiskLevel.LOW],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
ConditionalAccessGrantControl.PASSWORD_CHANGE,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' requires password change and MFA for user risk but does not include 'high' risk level."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == id
assert result[0].location == "global"
def test_policy_enabled_for_reporting_only(self):
"""Test FAIL when policy is correct but only in report-only mode."""
id = str(uuid4())
display_name = "Test"
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
),
user_risk_levels=[RiskLevel.HIGH],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
ConditionalAccessGrantControl.PASSWORD_CHANGE,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' requires password change and MFA for high-risk users but is set to report-only mode and does not enforce it."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == id
assert result[0].location == "global"
def test_policy_missing_password_change_grant(self):
"""Test FAIL when policy has MFA but not password change."""
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name="Test",
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=[],
),
user_risk_levels=[RiskLevel.HIGH],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires password change and MFA for high-risk users."
)
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_enabled_pass(self):
"""Test PASS when policy is properly configured and enabled."""
id = str(uuid4())
display_name = "Test"
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
),
user_risk_levels=[RiskLevel.HIGH],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
ConditionalAccessGrantControl.PASSWORD_CHANGE,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' requires password change and MFA for high-risk users."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == id
assert result[0].location == "global"
def test_policy_not_including_all_users(self):
"""Test FAIL when policy does not include all users."""
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name="Test",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["some-specific-user-id"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
user_risk_levels=[RiskLevel.HIGH],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
ConditionalAccessGrantControl.PASSWORD_CHANGE,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires password change and MFA for high-risk users."
)
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_not_including_all_applications(self):
"""Test FAIL when policy does not include all cloud applications."""
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name="Test",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["some-specific-app-id"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
user_risk_levels=[RiskLevel.HIGH],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
ConditionalAccessGrantControl.PASSWORD_CHANGE,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires password change and MFA for high-risk users."
)
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_with_or_operator(self):
"""Test FAIL when policy uses OR operator instead of AND for grant controls."""
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_require_password_change_high_risk_users.entra_conditional_access_policy_require_password_change_high_risk_users import (
entra_conditional_access_policy_require_password_change_high_risk_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name="Test",
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=[],
),
user_risk_levels=[RiskLevel.HIGH],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
ConditionalAccessGrantControl.PASSWORD_CHANGE,
],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = (
entra_conditional_access_policy_require_password_change_high_risk_users()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires password change and MFA for high-risk users."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"