mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-01 13:47:21 +00:00
Compare commits
1 Commits
chore/bump
...
feat/prowl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6360df6e62 |
@@ -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)
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user