feat(entra): add new check entra_identity_protection_user_risk_enabled (#7126)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Hugo Pereira Brito
2025-03-12 12:44:31 +01:00
committed by Pepe Fagoaga
parent 3116fef89a
commit bb1b81455c
5 changed files with 494 additions and 0 deletions
@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "entra_identity_protection_user_risk_enabled",
"CheckTitle": "Ensure that Identity Protection user risk policies are enabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Conditional Access Policy",
"Description": "Ensure that Identity Protection user risk policies are enabled to detect and respond to high risk potential account compromises.",
"Risk": "Without Identity Protection user risk policies enabled, compromised accounts may go undetected, allowing attackers to exploit breached credentials and gain unauthorized access. The absence of automated responses to user risk levels increases the likelihood of security incidents, such as data breaches or privilege escalation.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. 4. Set the following conditions within the policy: Under Users or workload identities choose All users. Under Cloud apps or actions choose All cloud apps. Under Conditions choose User risk then Yes and select the user risk level High. Under Access Controls select Grant then in the right pane click Grant access then select Require multifactor authentication and Require password change. Under Session ensure Sign-in frequency is set to Every time. Click Select. 5. Under Enable policy set it to Report Only until the organization is ready to enable it. 6. Click Create.",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Identity Protection user risk policies to detect and respond to potential account compromises. Configure Conditional Access policies to enforce MFA or password resets when a high user risk level is detected. Regularly review the Risky Users section to assess potential threats before enforcing strict access controls.",
"Url": "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,76 @@
from prowler.lib.check.models import Check, CheckReportMicrosoft365
from prowler.providers.microsoft365.services.entra.entra_client import entra_client
from prowler.providers.microsoft365.services.entra.entra_service import (
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
GrantControlOperator,
RiskLevel,
)
class entra_identity_protection_user_risk_enabled(Check):
"""Check if at least one Conditional Access policy is a Identity Protection user risk policy.
This check ensures that at least one Conditional Access policy is a Identity Protection user risk policy.
"""
def execute(self) -> list[CheckReportMicrosoft365]:
"""Execute the check to ensure that at least one Conditional Access policy is a Identity Protection user risk policy.
Returns:
list[CheckReportMicrosoft365]: A list containing the results of the check.
"""
findings = []
report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource={},
resource_name="Conditional Access Policies",
resource_id="conditionalAccessPolicies",
)
report.status = "FAIL"
report.status_extended = "No Conditional Access Policy is an user risk based Identity Protection Policy."
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 policy.conditions.user_risk_levels:
report = CheckReportMicrosoft365(
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}' is an user risk based Identity Protection Policy but does not protect against high risk potential account compromises."
elif policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is an user risk based Identity Protection Policy and reports high risk potential account compromises, but does not protect against them."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is an user risk based Identity Protection Policy and does protect against high risk potential account compromises."
break
findings.append(report)
return findings
@@ -170,6 +170,14 @@ class Entra(Microsoft365Service):
)
],
),
user_risk_levels=[
RiskLevel(risk_level)
for risk_level in getattr(
policy.conditions,
"user_risk_levels",
[],
)
],
),
grant_controls=GrantControls(
built_in_controls=(
@@ -328,9 +336,17 @@ class UsersConditions(BaseModel):
excluded_roles: List[str]
class RiskLevel(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
NO_RISK = "none"
class Conditions(BaseModel):
application_conditions: Optional[ApplicationsConditions]
user_conditions: Optional[UsersConditions]
user_risk_levels: List[RiskLevel] = []
class PersistentBrowser(BaseModel):
@@ -364,6 +380,7 @@ class ConditionalAccessGrantControl(Enum):
MFA = "mfa"
BLOCK = "block"
DOMAIN_JOINED_DEVICE = "domainJoinedDevice"
PASSWORD_CHANGE = "passwordChange"
class GrantControlOperator(Enum):
@@ -0,0 +1,371 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.microsoft365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
PersistentBrowser,
RiskLevel,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
class Test_entra_identity_protection_user_risk_enabled:
def test_entra_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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled import (
entra_identity_protection_user_risk_enabled,
)
entra_client.conditional_access_policies = {}
check = entra_identity_protection_user_risk_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy is an user risk based Identity Protection Policy."
)
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_entra_identity_protection_user_risk_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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled import (
entra_identity_protection_user_risk_enabled,
)
from prowler.providers.microsoft365.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=[], excluded_applications=[]
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[], 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.DISABLED,
)
}
check = entra_identity_protection_user_risk_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy is an user risk based Identity Protection Policy."
)
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_entra_identity_protection_user_risk_policy_enabled_not_enough_risk(self):
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled import (
entra_identity_protection_user_risk_enabled,
)
from prowler.providers.microsoft365.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=[],
),
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_FOR_REPORTING,
)
}
check = entra_identity_protection_user_risk_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' is an user risk based Identity Protection Policy but does not protect against high risk potential account compromises."
)
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_entra_identity_protection_user_risk_policy_enabled_for_reporting(self):
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled import (
entra_identity_protection_user_risk_enabled,
)
from prowler.providers.microsoft365.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=[],
),
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_identity_protection_user_risk_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' is an user risk based Identity Protection Policy and reports high risk potential account compromises, but does not protect against them."
)
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_entra_identity_protection_user_risk_policy_enabled(self):
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_identity_protection_user_risk_enabled.entra_identity_protection_user_risk_enabled import (
entra_identity_protection_user_risk_enabled,
)
from prowler.providers.microsoft365.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=[],
),
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_identity_protection_user_risk_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' is an user risk based Identity Protection Policy and does protect against high risk potential account compromises."
)
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"