mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-19 02:21:52 +00:00
feat(entra): add new check entra_legacy_authentication_blocked (#7240)
This commit is contained in:
committed by
Pepe Fagoaga
parent
71f8bcefa8
commit
b3d7bb4e8d
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "microsoft365",
|
||||
"CheckID": "entra_legacy_authentication_blocked",
|
||||
"CheckTitle": "Ensure that Conditional Access policy blocks legacy authentication",
|
||||
"CheckType": [],
|
||||
"ServiceName": "critical",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Conditional Access Policy",
|
||||
"Description": "Ensure that Conditional Access policy blocks legacy authentication in Microsoft Entra ID to enforce modern authentication methods and protect against credential-stuffing and brute-force attacks.",
|
||||
"Risk": "Legacy authentication protocols do not support MFA, making them vulnerable to credential-stuffing and brute-force attacks. Attackers commonly exploit these protocols to bypass security controls and gain unauthorized access.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy-authentication",
|
||||
"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. Under Users include All users. Under Target resources include All cloud apps and do not create any exclusions. Under Conditions select Client apps and check the boxes for Exchange ActiveSync clients and Other clients. Under Grant select Block Access. Click Select. 4. Set the policy On and click Create.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enforce Conditional Access policies to block legacy authentication across all users in Microsoft Entra ID. Ensure all applications and devices use modern authentication methods such as OAuth 2.0. For necessary exceptions (e.g., multifunction printers), configure secure alternatives following Microsoft's mail flow best practices.",
|
||||
"Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy-authentication"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
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 (
|
||||
ClientAppType,
|
||||
ConditionalAccessGrantControl,
|
||||
ConditionalAccessPolicyState,
|
||||
)
|
||||
|
||||
|
||||
class entra_legacy_authentication_blocked(Check):
|
||||
"""Check if at least one Conditional Access policy blocks legacy authentication.
|
||||
|
||||
This check ensures that at least one Conditional Access policy blocks legacy authentication.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportMicrosoft365]:
|
||||
"""Execute the check to ensure that at least one Conditional Access policy blocks legacy authentication.
|
||||
|
||||
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 blocks legacy authentication."
|
||||
)
|
||||
|
||||
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 (
|
||||
ClientAppType.EXCHANGE_ACTIVE_SYNC
|
||||
not in policy.conditions.client_app_types
|
||||
or ClientAppType.OTHER_CLIENTS not in policy.conditions.client_app_types
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
ConditionalAccessGrantControl.BLOCK
|
||||
in policy.grant_controls.built_in_controls
|
||||
):
|
||||
report = CheckReportMicrosoft365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.display_name,
|
||||
resource_id=policy.id,
|
||||
)
|
||||
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports legacy authentication but does not block it."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks legacy authentication."
|
||||
break
|
||||
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -181,6 +181,14 @@ class Entra(Microsoft365Service):
|
||||
)
|
||||
],
|
||||
),
|
||||
client_app_types=[
|
||||
ClientAppType(client_app_type)
|
||||
for client_app_type in getattr(
|
||||
policy.conditions,
|
||||
"client_app_types",
|
||||
[],
|
||||
)
|
||||
],
|
||||
user_risk_levels=[
|
||||
RiskLevel(risk_level)
|
||||
for risk_level in getattr(
|
||||
@@ -376,9 +384,18 @@ class RiskLevel(Enum):
|
||||
NO_RISK = "none"
|
||||
|
||||
|
||||
class ClientAppType(Enum):
|
||||
ALL = "all"
|
||||
BROWSER = "browser"
|
||||
MOBILE_APPS_AND_DESKTOP_CLIENTS = "mobileAppsAndDesktopClients"
|
||||
EXCHANGE_ACTIVE_SYNC = "exchangeActiveSync"
|
||||
OTHER_CLIENTS = "other"
|
||||
|
||||
|
||||
class Conditions(BaseModel):
|
||||
application_conditions: Optional[ApplicationsConditions]
|
||||
user_conditions: Optional[UsersConditions]
|
||||
client_app_types: Optional[List[ClientAppType]]
|
||||
user_risk_levels: List[RiskLevel] = []
|
||||
sign_in_risk_levels: List[RiskLevel] = []
|
||||
|
||||
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.microsoft365.services.entra.entra_service import (
|
||||
ApplicationsConditions,
|
||||
ClientAppType,
|
||||
ConditionalAccessGrantControl,
|
||||
ConditionalAccessPolicyState,
|
||||
Conditions,
|
||||
GrantControlOperator,
|
||||
GrantControls,
|
||||
PersistentBrowser,
|
||||
SessionControls,
|
||||
SignInFrequency,
|
||||
SignInFrequencyInterval,
|
||||
UsersConditions,
|
||||
)
|
||||
from tests.providers.microsoft365.microsoft365_fixtures import (
|
||||
DOMAIN,
|
||||
set_mocked_microsoft365_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_entra_legacy_authentication_blocked:
|
||||
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_legacy_authentication_blocked.entra_legacy_authentication_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_legacy_authentication_blocked.entra_legacy_authentication_blocked import (
|
||||
entra_legacy_authentication_blocked,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {}
|
||||
|
||||
check = entra_legacy_authentication_blocked()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy blocks legacy authentication."
|
||||
)
|
||||
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_block_legacy_authentication_disabled(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_legacy_authentication_blocked.entra_legacy_authentication_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_legacy_authentication_blocked.entra_legacy_authentication_blocked import (
|
||||
entra_legacy_authentication_blocked,
|
||||
)
|
||||
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=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[
|
||||
ClientAppType.EXCHANGE_ACTIVE_SYNC,
|
||||
ClientAppType.OTHER_CLIENTS,
|
||||
],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[
|
||||
ConditionalAccessGrantControl.BLOCK,
|
||||
],
|
||||
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_legacy_authentication_blocked()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy blocks legacy authentication."
|
||||
)
|
||||
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_block_legacy_authentication_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_legacy_authentication_blocked.entra_legacy_authentication_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_legacy_authentication_blocked.entra_legacy_authentication_blocked import (
|
||||
entra_legacy_authentication_blocked,
|
||||
)
|
||||
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=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[
|
||||
ClientAppType.EXCHANGE_ACTIVE_SYNC,
|
||||
ClientAppType.OTHER_CLIENTS,
|
||||
],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[
|
||||
ConditionalAccessGrantControl.BLOCK,
|
||||
],
|
||||
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_legacy_authentication_blocked()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' reports legacy authentication but does not block 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_entra_block_legacy_authentication_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_legacy_authentication_blocked.entra_legacy_authentication_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_legacy_authentication_blocked.entra_legacy_authentication_blocked import (
|
||||
entra_legacy_authentication_blocked,
|
||||
)
|
||||
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=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[],
|
||||
excluded_roles=[],
|
||||
),
|
||||
client_app_types=[
|
||||
ClientAppType.EXCHANGE_ACTIVE_SYNC,
|
||||
ClientAppType.OTHER_CLIENTS,
|
||||
],
|
||||
user_risk_levels=[],
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[
|
||||
ConditionalAccessGrantControl.BLOCK,
|
||||
],
|
||||
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_legacy_authentication_blocked()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' blocks legacy authentication."
|
||||
)
|
||||
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"
|
||||
Reference in New Issue
Block a user