Compare commits

...

1 Commits

Author SHA1 Message Date
HugoPBrito
3217325002 feat(m365): add entra_conditional_access_policy_risky_sign_in_mfa security check
Add new security check entra_conditional_access_policy_risky_sign_in_mfa for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-02-25 10:31:58 +01:00
9 changed files with 809 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_risky_sign_in_mfa` check for m365 provider [(#10168)](https://github.com/prowler-cloud/prowler/pull/10168)
- `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

@@ -1260,6 +1260,7 @@
"Id": "5.2.2.7",
"Description": "Microsoft Entra ID Protection sign-in risk detects risks in real-time and offline. A risky sign-in is an indicator for a sign-in attempt that might not have been performed by the legitimate owner of a user account.**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_risky_sign_in_mfa",
"entra_identity_protection_sign_in_risk_enabled"
],
"Attributes": [

View File

@@ -1492,6 +1492,7 @@
"Id": "5.2.2.7",
"Description": "Microsoft Entra ID Protection sign-in risk detects risks in real-time and offline. A risky sign-in is an indicator for a sign-in attempt that might not have been performed by the legitimate owner of a user account. Enable Identity Protection sign-in risk policies.",
"Checks": [
"entra_conditional_access_policy_risky_sign_in_mfa",
"entra_identity_protection_sign_in_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_risky_sign_in_mfa",
"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_risky_sign_in_mfa",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]
@@ -240,6 +242,7 @@
"entra_admin_users_sign_in_frequency_enabled",
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_conditional_access_policy_risky_sign_in_mfa",
"entra_legacy_authentication_blocked",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
@@ -685,6 +688,7 @@
"Checks": [
"entra_admin_users_sign_in_frequency_enabled",
"entra_admin_users_mfa_enabled",
"entra_conditional_access_policy_risky_sign_in_mfa",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
"entra_users_mfa_enabled",

View File

@@ -406,6 +406,7 @@
"Id": "1.2.5",
"Description": "Enable Identity Protection sign-in risk policies ",
"Checks": [
"entra_conditional_access_policy_risky_sign_in_mfa",
"entra_identity_protection_sign_in_risk_enabled"
],
"Attributes": [

View File

@@ -0,0 +1,40 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_risky_sign_in_mfa",
"CheckTitle": "Conditional Access Policy requires MFA for medium and high risk sign-ins",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Conditional Access Policy",
"ResourceGroup": "IAM",
"Description": "A Conditional Access policy should require **multifactor authentication** for sign-ins classified as `medium` or `high` risk, targeting all users and all cloud applications.\n\nThe policy must also enforce **sign-in frequency** set to `everyTime` to require re-authentication on each risky sign-in attempt.",
"Risk": "Without a Conditional Access policy enforcing MFA on risky sign-ins, attackers who compromise credentials can authenticate without additional verification. This exposes the tenant to **unauthorized access**, **lateral movement**, and **data exfiltration** from accounts flagged as risky by Identity Protection.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-risk",
"https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center (https://entra.microsoft.com).\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Select **New policy**.\n4. Under **Users**, select **All users**.\n5. Under **Cloud apps or actions**, select **All cloud apps**.\n6. Under **Conditions** > **Sign-in risk**, set to **Yes** and check **High** and **Medium**.\n7. Under **Grant**, select **Require multifactor authentication**.\n8. Under **Session** > **Sign-in frequency**, select **Every time**.\n9. Set **Enable policy** to **On** and click **Create**.",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure a Conditional Access policy to enforce MFA on risky sign-ins. Use Entra ID Identity Protection risk signals to target `medium` and `high` risk levels, and set sign-in frequency to **every time** to prevent session reuse after risk detection.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_risky_sign_in_mfa"
}
},
"Categories": [
"identity-access",
"e5"
],
"DependsOn": [],
"RelatedTo": [
"entra_identity_protection_sign_in_risk_enabled"
],
"Notes": "Requires Entra ID P2 license. Legacy risk policies in Identity Protection retire October 2026 - must use Conditional Access policies."
}

View File

@@ -0,0 +1,86 @@
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,
RiskLevel,
SignInFrequencyInterval,
)
class entra_conditional_access_policy_risky_sign_in_mfa(Check):
"""Check if a Conditional Access policy requires MFA for medium and high risk sign-ins.
This check verifies that at least one enabled Conditional Access policy is configured to
require multifactor authentication for risky sign-ins, targeting all users and all applications,
with sign-in frequency set to every time to force re-authentication.
- PASS: An enabled CA policy requires MFA for medium and high risk sign-ins with every-time sign-in frequency.
- FAIL: No CA policy addresses risky sign-ins with MFA requirement, or the policy is missing required conditions.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check for Conditional Access policy requiring MFA on risky sign-ins.
Returns:
list[CheckReportM365]: A list containing the results of the check.
"""
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 MFA for risky sign-ins."
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.MFA
not in policy.grant_controls.built_in_controls
):
continue
if (
SignInFrequencyInterval.EVERY_TIME
!= policy.session_controls.sign_in_frequency.interval
):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.display_name,
resource_id=policy.id,
)
has_high = RiskLevel.HIGH in policy.conditions.sign_in_risk_levels
has_medium = RiskLevel.MEDIUM in policy.conditions.sign_in_risk_levels
if not has_high or not has_medium:
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires MFA but does not cover both high and medium sign-in risk levels."
elif policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires MFA for high and medium risk sign-ins but is set to report-only mode and does not enforce protection."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires MFA for high and medium risk sign-ins and enforces re-authentication every time."
break
findings.append(report)
return findings

View File

@@ -0,0 +1,675 @@
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa"
class Test_entra_conditional_access_policy_risky_sign_in_mfa:
def test_no_conditional_access_policies(self):
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
entra_client.conditional_access_policies = {}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for risky sign-ins."
)
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):
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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=[],
sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for risky sign-ins."
)
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_missing_risk_levels(self):
id = str(uuid4())
display_name = "Risky Sign-in Policy"
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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=[],
sign_in_risk_levels=[RiskLevel.LOW],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' requires MFA but does not cover both high and medium sign-in risk levels."
)
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_report_only_mode(self):
id = str(uuid4())
display_name = "Risky Sign-in Policy"
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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=[],
sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' requires MFA for high and medium risk sign-ins but is set to report-only mode and does not enforce protection."
)
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_no_mfa_grant_control(self):
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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=[],
sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for risky sign-ins."
)
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_all_users(self):
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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-user-id"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
user_risk_levels=[],
sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for risky sign-ins."
)
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_all_applications(self):
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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-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=[],
sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for risky sign-ins."
)
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_sign_in_frequency_not_every_time(self):
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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=[],
sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=1,
type=None,
interval=SignInFrequencyInterval.TIME_BASED,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for risky sign-ins."
)
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_fully_compliant(self):
id = str(uuid4())
display_name = "Risky Sign-in MFA Policy"
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_risky_sign_in_mfa.entra_conditional_access_policy_risky_sign_in_mfa import (
entra_conditional_access_policy_risky_sign_in_mfa,
)
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=[],
sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=True,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_risky_sign_in_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' requires MFA for high and medium risk sign-ins and enforces re-authentication every time."
)
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"