Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(microsoft365): add new check entra_admin_users_sign_in_frequency_enabled #7020

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "entra_admin_users_sign_in_frequency_enabled",
"CheckTitle": "",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "",
"Description": "Ensure Sign-in frequency periodic reauthentication does not exceed 4 hours for E3 tenants, or 24 hours for E5 tenants using Privileged Identity Management.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not checking the period of the sign-in frequency, only if it is enabled. Check if it is necessary to check the period, please.

"Risk": "Allowing persistent browser sessions and long sign-in frequencies for administrative users increases the risk of unauthorized access. Attackers could exploit session persistence to maintain access to an admin account without reauthentication, increasing the likelihood of account compromise, especially in cases of credential theft or session hijacking.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-session#sign-in-frequency",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Protection > Conditional Access Select Policies. 3. Click New policy. Under Users include, select users and groups and check Directory roles. At a minimum, include the directory roles listed below in this section of the document. Under Target resources, include All cloud apps and do not create any exclusions. Under Grant, select Grant Access and check Require multifactor authentication. Under Session, select Sign-in frequency, select Periodic reauthentication, and set it to 4 hours for E3 tenants. E5 tenants with PIM can be set to a maximum value of 24 hours. Check Persistent browser session, then select Never persistent in the drop-down menu. 4. Under Enable policy, set it to Report Only until the organization is ready to enable it.",
"Terraform": ""
},
"Recommendation": {
"Text": "Enforce a sign-in frequency limit of no more than 4 hours for E3 tenants (or 24 hours for E5 with Privileged Identity Management) and set browser sessions to Never persistent. This ensures that administrative users are regularly reauthenticated, reducing the risk of prolonged unauthorized access and mitigating session hijacking threats.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime#user-sign-in-frequency"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 (
AdminRoles,
ConditionalAccessPolicyState,
)


class entra_admin_users_sign_in_frequency_enabled(Check):
"""Check if Conditional Access policies enforce sign-in frequency for admin users.

This check ensures that administrators have a sign-in frequency policy enabled
and that persistent browser session settings are correctly configured.
"""

def execute(self) -> list[CheckReportMicrosoft365]:
"""Execute the check to validate sign-in frequency enforcement for admin users.

Returns:
list[CheckReportMicrosoft365]: A list containing the results of the check.
"""
findings = []

report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource=entra_client.conditional_access_policies,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
resource=entra_client.conditional_access_policies,
resource={},

We cannot pass a dict as a resource.

resource_name="Conditional Access Policies",
resource_id="conditionalAccessPolicies",
)
report.status = "FAIL"
report.status_extended = (
"No Conditional Access policy enforces sign-in frequency for admin users."
)

for policy in entra_client.conditional_access_policies.values():
if policy.state not in {
ConditionalAccessPolicyState.ENABLED,
ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
}:
continue

if not {role.value for role in AdminRoles}.issuperset(
policy.conditions.user_conditions.included_roles
):
continue

Check warning on line 45 in prowler/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.py#L45

Added line #L45 was not covered by tests

if (
"All"
not in policy.conditions.application_conditions.included_applications
):
continue

Check warning on line 51 in prowler/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/microsoft365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.py#L51

Added line #L51 was not covered by tests

if (
policy.session_controls.sign_in_frequency.is_enabled
and policy.session_controls.sign_in_frequency.frequency
and policy.session_controls.sign_in_frequency.frequency <= 4
and policy.session_controls.persistent_browser.is_enabled
and policy.session_controls.persistent_browser.mode == "never"
):
report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource=entra_client.conditional_access_policies,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
resource=entra_client.conditional_access_policies,
resource=policy,

Same here, we cannot pass the entire dictionary as a resource.

resource_name=policy.display_name,
resource_id=policy.id,
)
report.status = "PASS"
report.status_extended = f"Conditional Access policy {policy.display_name} enforces sign-in frequency for admin users."
break

findings.append(report)

return findings
191 changes: 191 additions & 0 deletions prowler/providers/microsoft365/services/entra/entra_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from asyncio import gather, get_event_loop
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel
Expand All @@ -17,10 +18,12 @@
attributes = loop.run_until_complete(
gather(
self._get_authorization_policy(),
self._get_conditional_access_policies(),
)
)

self.authorization_policy = attributes[0]
self.conditional_access_policies = attributes[1]

async def _get_authorization_policy(self):
logger.info("Entra - Getting authorization policy...")
Expand Down Expand Up @@ -83,6 +86,176 @@

return authorization_policy

async def _get_conditional_access_policies(self):
logger.info("Entra - Getting conditional access policies...")

conditional_access_policies = {}
try:
conditional_access_policies_list = (
await self.client.identity.conditional_access.policies.get()
)
for policy in conditional_access_policies_list.value:
conditional_access_policies[policy.id] = ConditionalAccessPolicy(

Check warning on line 98 in prowler/providers/microsoft365/services/entra/entra_service.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/microsoft365/services/entra/entra_service.py#L97-L98

Added lines #L97 - L98 were not covered by tests
id=policy.id,
display_name=policy.display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=[
application
for application in getattr(
policy.conditions.applications,
"include_applications",
[],
)
],
excluded_applications=[
application
for application in getattr(
policy.conditions.applications,
"exclude_applications",
[],
)
],
),
user_conditions=UsersConditions(
included_groups=[
group
for group in getattr(
policy.conditions.users,
"include_groups",
[],
)
],
excluded_groups=[
group
for group in getattr(
policy.conditions.users,
"exclude_groups",
[],
)
],
included_users=[
user
for user in getattr(
policy.conditions.users,
"include_users",
[],
)
],
excluded_users=[
user
for user in getattr(
policy.conditions.users,
"exclude_users",
[],
)
],
included_roles=[
role
for role in getattr(
policy.conditions.users,
"include_roles",
[],
)
],
excluded_roles=[
role
for role in getattr(
policy.conditions.users,
"exclude_roles",
[],
)
],
),
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=(
policy.session_controls.persistent_browser.is_enabled
if policy.session_controls
and policy.session_controls.persistent_browser
else False
),
mode=(
policy.session_controls.persistent_browser.mode
if policy.session_controls
and policy.session_controls.persistent_browser
else "always"
),
),
sign_in_frequency=SignInFrequency(
is_enabled=(
policy.session_controls.sign_in_frequency.is_enabled
if policy.session_controls
and policy.session_controls.sign_in_frequency
else False
),
frequency=(
policy.session_controls.sign_in_frequency.value
if policy.session_controls
and policy.session_controls.sign_in_frequency
else None
),
),
),
state=ConditionalAccessPolicyState(
getattr(policy, "state", "disabled")
),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return conditional_access_policies


class ConditionalAccessPolicyState(Enum):
ENABLED = "enabled"
DISABLED = "disabled"
ENABLED_FOR_REPORTING = "enabledForReportingButNotEnforced"


class ApplicationsConditions(BaseModel):
included_applications: List[str]
excluded_applications: List[str]


class UsersConditions(BaseModel):
included_groups: List[str]
excluded_groups: List[str]
included_users: List[str]
excluded_users: List[str]
included_roles: List[str]
excluded_roles: List[str]


class Conditions(BaseModel):
application_conditions: Optional[ApplicationsConditions]
user_conditions: Optional[UsersConditions]


class PersistentBrowser(BaseModel):
is_enabled: bool
mode: str


class SignInFrequency(BaseModel):
is_enabled: bool
frequency: Optional[int]


class SessionControls(BaseModel):
persistent_browser: PersistentBrowser
sign_in_frequency: SignInFrequency


class ConditionalAccessPolicy(BaseModel):
id: str
display_name: str
conditions: Conditions
session_controls: SessionControls
state: ConditionalAccessPolicyState


class DefaultUserRolePermissions(BaseModel):
allowed_to_create_apps: Optional[bool]
Expand All @@ -99,3 +272,21 @@
name: str
description: str
default_user_role_permissions: Optional[DefaultUserRolePermissions]


class AdminRoles(Enum):
APPLICATION_ADMINISTRATOR = "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3"
AUTHENTICATION_ADMINISTRATOR = "c4e39bd9-1100-46d3-8c65-fb160da0071f"
BILLING_ADMINISTRATOR = "b0f54661-2d74-4c50-afa3-1ec803f12efe"
CLOUD_APPLICATION_ADMINISTRATOR = "158c047a-c907-4556-b7ef-446551a6b5f7"
CONDITIONAL_ACCESS_ADMINISTRATOR = "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9"
EXCHANGE_ADMINISTRATOR = "29232cdf-9323-42fd-ade2-1d097af3e4de"
GLOBAL_ADMINISTRATOR = "62e90394-69f5-4237-9190-012177145e10"
GLOBAL_READER = "f2ef992c-3afb-46b9-b7cf-a126ee74c451"
HELPDESK_ADMINISTRATOR = "729827e3-9c14-49f7-bb1b-9608f156bbb8"
PASSWORD_ADMINISTRATOR = "966707d0-3269-4727-9be2-8c3a10f19b9d"
PRIVILEGED_AUTHENTICATION_ADMINISTRATOR = "7be44c8a-adaf-4e2a-84d6-ab2649e08a13"
PRIVILEGED_ROLE_ADMINISTRATOR = "e8611ab8-c189-46e8-94e1-60213ab1f814"
SECURITY_ADMINISTRATOR = "194ae4cb-b126-40b2-bd5b-6091b380977d"
SHAREPOINT_ADMINISTRATOR = "f28a1f50-f6e7-4571-818b-6a12f2af6b6c"
USER_ADMINISTRATOR = "fe930be7-5e62-47db-91af-98c3a49a38b1"
Loading