diff --git a/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/__init__.py b/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.metadata.json b/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.metadata.json new file mode 100644 index 0000000000..04f94276b9 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.metadata.json @@ -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": "" +} diff --git a/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.py b/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.py new file mode 100644 index 0000000000..f6cb547474 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.py @@ -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 diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py index c75add941b..97d347fba7 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -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): diff --git a/tests/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled_test.py new file mode 100644 index 0000000000..e1f7626701 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled_test.py @@ -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"