feat(m365): add 37 automated checks

This commit is contained in:
pedrooot committed 2026-07-23 14:52:38 +02:00
1 parent fb75146e34
commit 3d50bed244
203 files changed
+7179 -46

No files matched your search

@@ -0,0 +1 @@
37 M365 checks covering the CIS Microsoft 365 Foundations Benchmark v7.0.0 Automated controls across the `entra`, `admincenter`, `exchange`, `teams`, `intune`, and `defender` services
+112 -37
View File
@@ -170,7 +170,10 @@
{
"Id": "1.3.2",
"Description": "Idle session timeout allows the configuration of a setting which will timeout inactive users after a pre-determined amount of time. When a user reaches the set idle timeout session, they'll get a notification that they're about to be signed out. They must choose to stay signed in or they'll be automatically signed out of all Microsoft 365 web apps. Combined with a Conditional Access rule this will only impact unmanaged devices. A managed device is considered a device managed by Intune MDM or joined to a domain (Entra ID or Hybrid joined). The following Microsoft 365 web apps are supported. - Outlook Web App - OneDrive - SharePoint - Microsoft Fabric - Microsoft365.com and other start pages - Microsoft 365 web apps (Word, Excel, PowerPoint) - Microsoft 365 Admin Center - M365 Defender Portal - Microsoft Purview Compliance Portal The recommended setting is 3 hours (or less) for unmanaged devices. Note: Idle session timeout doesn't affect Microsoft 365 desktop and mobile apps.",
"Checks": [],
"Checks": [
"entra_idle_session_timeout_configured",
"entra_conditional_access_policy_app_enforced_restrictions"
],
"Attributes": [
{
"Section": "1 Microsoft 365 admin center",
@@ -214,7 +217,9 @@
{
"Id": "1.3.4",
"Description": "By default, users can install add-ins in their Microsoft Word, Excel, and PowerPoint applications, allowing data access within the application. Do not allow users to install add-ins in Word, Excel, or PowerPoint.",
"Checks": [],
"Checks": [
"admincenter_office_store_apps_restricted"
],
"Attributes": [
{
"Section": "1 Microsoft 365 admin center",
@@ -235,7 +240,9 @@
{
"Id": "1.3.5",
"Description": "Microsoft Forms can be used for phishing attacks by asking personal or sensitive information and collecting the results. Microsoft 365 has built-in protection that will proactively scan for phishing attempt in forms such personal information request.",
"Checks": [],
"Checks": [
"admincenter_forms_phishing_protection_enabled"
],
"Attributes": [
{
"Section": "1 Microsoft 365 admin center",
@@ -323,7 +330,9 @@
{
"Id": "1.3.9",
"Description": "Shared Bookings allows you to invite your team members and create booking pages and let your customers book time with you and your team. It contains various settings to define services, manage staff members, configure schedules and availability, business hours and customize how appointments are scheduled. These pages can be customized to fit the diverse needs of your organization. It is an extension of Person Bookings. The recommended state is to restrict the OwaMailboxPolicy-Default policy or disable at the organization level.",
"Checks": [],
"Checks": [
"admincenter_shared_bookings_disabled"
],
"Attributes": [
{
"Section": "1 Microsoft 365 admin center",
@@ -768,7 +777,9 @@
{
"Id": "2.4.1",
"Description": "Identify priority accounts to utilize Microsoft 365's advanced custom security features. This is an essential tool to bolster protection for users who are frequently targeted due to their critical positions, such as executives, leaders, managers, or others who have access to sensitive, confidential, financial, or high-priority information. Once these accounts are identified, several services and features can be enabled, including threat policies, enhanced sign-in protection through conditional access policies, and alert policies, enabling faster response times for incident response teams.",
"Checks": [],
"Checks": [
"defender_priority_account_protection_enabled"
],
"Attributes": [
{
"Section": "2 Microsoft Defender",
@@ -789,7 +800,9 @@
{
"Id": "2.4.2",
"Description": "Preset security policies have been established by Microsoft, utilizing observations and experiences within datacenters to strike a balance between the exclusion of malicious content from users and limiting unwarranted disruptions. These policies can apply to all, or select users and encompass recommendations for addressing spam, malware, and phishing threats. The policy parameters are pre-determined and non-adjustable. Strict protection has the most aggressive protection of the 3 presets. - EOP: Anti-spam, Anti-malware and Anti-phishing - Defender: Spoof protection, Impersonation protection and Advanced phishing - Defender: Safe Links and Safe Attachments NOTE: The preset security polices cannot target Priority account TAGS currently, groups should be used instead.",
"Checks": [],
"Checks": [
"defender_strict_preset_security_policy_enabled"
],
"Attributes": [
{
"Section": "2 Microsoft Defender",
@@ -1005,7 +1018,9 @@
{
"Id": "4.2",
"Description": "Device enrollment restrictions let you restrict devices from enrolling in Intune based on certain device attributes such as device limit, device platform, OS Version, manufacturer or device ownership (Personally owned devices). The recommended state is to Block personally owned devices from enrollment.",
"Checks": [],
"Checks": [
"intune_device_enrollment_personal_device_restricted"
],
"Attributes": [
{
"Section": "4 Microsoft Intune admin center",
@@ -1160,7 +1175,9 @@
{
"Id": "5.1.3.1",
"Description": "This setting allows users in the organization to create new security groups and add members to these groups in the Azure portal, API, or PowerShell. These new groups also show up in the Access Panel for all other users. If the policy setting on the group allows it, other users can create requests to join these groups. The recommended state is Users can create security groups in Azure portals, API or PowerShell set to No.",
"Checks": [],
"Checks": [
"entra_policy_default_user_cannot_create_security_groups"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1223,7 +1240,9 @@
{
"Id": "5.1.3.4",
"Description": "All users within a Microsoft Entra organization are permitted to create new Microsoft 365 groups and add members to those groups through the Azure portal, API, or PowerShell. Newly created groups also appear in the Access Panel for all other users. When the applicable group policy settings allow it, users can submit requests to join these groups. The recommended state is No.",
"Checks": [],
"Checks": [
"entra_policy_default_user_cannot_create_m365_groups"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1244,7 +1263,9 @@
{
"Id": "5.1.4.1",
"Description": "This setting enables you to select the users who can register their devices as Microsoft Entra joined devices. The recommended state is Selected or None. Note: This setting is applicable only to Microsoft Entra join on Windows 10 or newer. This setting doesn't apply to Microsoft Entra hybrid joined devices, Microsoft Entra joined VMs in Azure, or Microsoft Entra joined devices that use Windows Autopilot self- deployment mode because these methods work in a userless context.",
"Checks": [],
"Checks": [
"entra_device_registration_join_restricted"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1265,7 +1286,9 @@
{
"Id": "5.1.4.2",
"Description": "This setting defines the maximum number of Microsoft Entra joined or registered devices that a user can have in Microsoft Entra ID. Once this limit is reached, no additional devices can be added until existing ones are removed. Values above 100 are automatically capped at 100. The recommended state is 10 or less.",
"Checks": [],
"Checks": [
"entra_device_registration_max_devices_per_user_limited"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1286,7 +1309,9 @@
{
"Id": "5.1.4.3",
"Description": "This setting controls whether the Global Administrator role is automatically added to the local administrators group on a device during the Microsoft Entra join process. The recommended state is No.",
"Checks": [],
"Checks": [
"entra_device_registration_global_admins_not_local_admins"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1307,7 +1332,9 @@
{
"Id": "5.1.4.4",
"Description": "This setting determines if the Microsoft Entra user registering their device as Microsoft Entra join will be added to the local administrators group. This setting applies only once during the actual registration of the device as Microsoft Entra join. The recommended state is Selected or None.",
"Checks": [],
"Checks": [
"entra_device_registration_registering_user_not_local_admin"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1328,7 +1355,9 @@
{
"Id": "5.1.4.5",
"Description": "Local Administrator Password Solution (LAPS) is the management of local account passwords on Windows devices. LAPS provides a solution to securely manage and retrieve the built-in local admin password. With cloud version of LAPS, customers can enable storing and rotation of local admin passwords for both Microsoft Entra and Microsoft Entra hybrid join devices The recommended state is Yes.",
"Checks": [],
"Checks": [
"entra_device_registration_laps_enabled"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1349,7 +1378,9 @@
{
"Id": "5.1.4.6",
"Description": "This setting determines if users can self-service recover their BitLocker key(s). 'Yes' restricts non-admin users from being able to see the BitLocker key(s) for their owned devices if there are any. 'No' allows all users to recover their BitLocker key(s). The recommended state is Yes.",
"Checks": [],
"Checks": [
"entra_policy_default_user_cannot_read_bitlocker_keys"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1508,7 +1539,9 @@
{
"Id": "5.1.6.1",
"Description": "B2B collaboration is a feature within Microsoft Entra External ID that allows for guest invitations to an organization. Ensure users can only send invitations to specified domains. Note: This list works independently from OneDrive for Business and SharePoint Online allow/block lists. To restrict individual file sharing in SharePoint Online, set up an allow or blocklist for OneDrive for Business and SharePoint Online. For instance, in SharePoint or OneDrive users can still share with external users from prohibited domains by using Anyone links if they haven't been disabled.",
"Checks": [],
"Checks": [
"entra_policy_guest_invitations_restricted_to_allowed_domains"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1759,7 +1792,9 @@
{
"Id": "5.2.2.8",
"Description": "Microsoft Entra ID Protection sign-in risk detects risks in real-time and offline. A risky sign-in is an indicator for a sign-in attempt that might not have been performed by the legitimate owner of a user account. 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": [],
"Checks": [
"entra_conditional_access_policy_block_high_medium_sign_in_risk"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1872,7 +1907,9 @@
{
"Id": "5.2.2.13",
"Description": "Sign-in frequency defines the time period before a user is asked to sign in again when attempting to access a resource. The Microsoft Entra ID default configuration for user sign-in frequency is a rolling window of 90 days. The recommended state for all users is to enforce periodic reauthentication for 7 days or less.",
"Checks": [],
"Checks": [
"entra_conditional_access_policy_sign_in_frequency_all_users"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1893,7 +1930,9 @@
{
"Id": "5.2.2.14",
"Description": "Microsoft Entra ID Conditional Access allows an organization to configure Named locations and configure whether those locations are trusted or untrusted. These settings provide organizations the means to specify Geographical locations for use in conditional access policies, or define actual IP addresses and IP ranges and whether or not those IP addresses and/or ranges are trusted by the organization. The recommended state is to define at least one trusted, IP range named location.",
"Checks": [],
"Checks": [
"entra_conditional_access_trusted_named_location_exists"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1914,7 +1953,9 @@
{
"Id": "5.2.2.15",
"Description": "Conditional Access Policies can be used to block access from geographic locations that are deemed out-of-scope for your organization or application. The scope and variables for this policy should be carefully examined and defined. The recommended state is to configure at least one policy to block access from untrusted locations.",
"Checks": [],
"Checks": [
"entra_conditional_access_policy_untrusted_locations_blocked"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1935,7 +1976,9 @@
{
"Id": "5.2.2.16",
"Description": "Token Protection is a Conditional Access session control that attempts to reduce token replay attacks by ensuring only device bound sign-in session tokens, like Primary Refresh Tokens (PRTs), are accepted by Microsoft Entra ID when applications request access to protected resources. When a user registers a supported device with Microsoft Entra, a PRT is issued and cryptographically bound to that device. This binding ensures that even if a threat actor steals the token, it can't be used from another device. With Token Protection enforced, Microsoft Entra validates that only these bound sign-in session tokens are used by supported applications. The recommended state is to enforce Token Protection for Office 365 Exchange Online, Office 365 SharePoint Online and Microsoft Teams Services.",
"Checks": [],
"Checks": [
"entra_conditional_access_policy_token_protection_enforced"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1956,7 +1999,9 @@
{
"Id": "5.2.2.17",
"Description": "Authentication transfer is a flow that lets users seamlessly transfer authenticated state from one device to another. For example, users might see a QR code in the desktop version of Outlook that, when scanned on their mobile device, transfers their authenticated state to the mobile device. The recommended state is to block Authentication transfer.",
"Checks": [],
"Checks": [
"entra_conditional_access_policy_authentication_transfer_blocked"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1977,7 +2022,9 @@
{
"Id": "5.2.3.1",
"Description": "Microsoft provides supporting settings to enhance the configuration of the Microsoft Authenticator application. These settings provide users with additional information and context when they receive MFA passwordless and push requests, including the geographic location of the request, the requesting application, and a requirement for number matching. The recommended state is Enabled for the following: - Show application name in push and passwordless notifications - Show geographic location in push and passwordless notifications Note: On February 27, 2023 Microsoft started enforcing number matching tenant-wide for all users using Microsoft Authenticator.",
"Checks": [],
"Checks": [
"entra_authentication_method_authenticator_show_context"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1998,7 +2045,9 @@
{
"Id": "5.2.3.2",
"Description": "With Entra Password Protection, default global banned password lists are automatically applied to all users in an Entra ID tenant. To support business and security needs, custom banned password lists can be defined. When users change or reset their passwords, these banned password lists are checked to enforce the use of strong passwords. A custom banned password list should include some of the following examples: - Brand names - Product names - Locations, such as company headquarters - Company-specific internal terms - Abbreviations that have specific company meaning",
"Checks": [],
"Checks": [
"entra_password_protection_custom_banned_list_enforced"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2019,7 +2068,9 @@
{
"Id": "5.2.3.3",
"Description": "Microsoft Entra Password Protection provides a global and custom banned password list. A password change request fails if there's a match in these banned password list. To protect on-premises Active Directory Domain Services (AD DS) environment, install and configure Entra Password Protection. Note: This recommendation applies to Hybrid deployments only and will have no impact unless working with on-premises Active Directory.",
"Checks": [],
"Checks": [
"entra_password_protection_on_premises_enforced"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2086,7 +2137,9 @@
{
"Id": "5.2.3.6",
"Description": "System-preferred multifactor authentication (MFA) prompts users to sign in by using the most secure method they registered. The user is prompted to sign-in with the most secure method according to the below order. The order of authentication methods is dynamic. It's updated by Microsoft as the security landscape changes, and as better authentication methods emerge. 1. Temporary Access Pass 2. Passkey (FIDO2) 3. Microsoft Authenticator notifications 4. External authentication methods 5. Time-based one-time password (TOTP) 6. Telephony 7. Certificate-based authentication The recommended state is Enabled.",
"Checks": [],
"Checks": [
"entra_authentication_method_system_preferred_mfa_enabled"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2107,7 +2160,9 @@
{
"Id": "5.2.3.7",
"Description": "Authentication methods support a wide variety of scenarios for signing in to Microsoft 365 resources. Some of these methods are inherently more secure than others but require more investment in time to get users enrolled and operational. The email one-time passcode feature is a way to authenticate B2B collaboration users when they can't be authenticated through other means, such as Microsoft Entra ID, Microsoft account (MSA), or social identity providers. When a B2B guest user tries to redeem your invitation or sign in to your shared resources, they can request a temporary passcode, which is sent to their email address. Then they enter this passcode to continue signing in. The recommended state is to Disable email OTP.",
"Checks": [],
"Checks": [
"entra_authentication_method_email_otp_disabled"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2128,7 +2183,9 @@
{
"Id": "5.2.3.8",
"Description": "The account lockout threshold determines how many failed login attempts are permitted prior to placing the account in a locked-out state and initiating a variable lockout duration. The recommended Lockout threshold is 10 or less.",
"Checks": [],
"Checks": [
"entra_password_protection_lockout_threshold_limited"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2149,7 +2206,9 @@
{
"Id": "5.2.3.9",
"Description": "The account lockout duration value determines how long an account retains the status of lockout, and therefore how long before a user can continue to attempt to login after passing the lockout threshold. The recommended state is Lockout duration in seconds is at least 60.",
"Checks": [],
"Checks": [
"entra_password_protection_lockout_duration_configured"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2170,7 +2229,9 @@
{
"Id": "5.2.3.10",
"Description": "Microsoft Entra ID includes a feature called Authenticator Lite, which embeds a subset of Microsoft Authenticator functionality into companion applications such as Outlook mobile. When enabled, users can satisfy MFA requirements using push notifications or time-based one-time passcodes (TOTP) directly from the companion application without installing the standalone Microsoft Authenticator app. The recommended state is Microsoft Authenticator on companion applications set to Disabled.",
"Checks": [],
"Checks": [
"entra_authentication_method_authenticator_companion_app_disabled"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2317,7 +2378,9 @@
{
"Id": "5.3.2",
"Description": "Access reviews enable administrators to establish an efficient automated process for reviewing group memberships, access to enterprise applications, and role assignments. These reviews can be scheduled to recur regularly, with flexible options for delegating the task of reviewing membership to different members of the organization. When configured for guest users, access reviews can automatically remove access if no reviewer responds within the review period, enforcing a fail-closed posture for external identities.",
"Checks": [],
"Checks": [
"entra_access_review_guest_users_configured"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2338,7 +2401,9 @@
{
"Id": "5.3.3",
"Description": "Access reviews in Microsoft Entra Privileged Identity Management (PIM) enable administrators to periodically validate whether users still require their privileged role assignments. These reviews can be scheduled to recur on a regular cadence and can be delegated to reviewers other than the role holders themselves, such as security auditors.",
"Checks": [],
"Checks": [
"entra_access_review_privileged_roles_configured"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2359,7 +2424,9 @@
{
"Id": "5.3.4",
"Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Requiring approval before activation allows one of the selected approvers to first review and then approve the activation prior to PIM granted the role. The approver doesn't have to be a group member or owner. The recommended state is Require approval to activate for the Global Administrator role.",
"Checks": [],
"Checks": [
"entra_pim_global_administrator_approval_required"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2380,7 +2447,9 @@
{
"Id": "5.3.5",
"Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Requiring approval before activation allows one of the selected approvers to first review and then approve the activation prior to PIM granted the role. The approver doesn't have to be a group member or owner. The recommended state is Require approval to activate for the Privileged Role Administrator role.",
"Checks": [],
"Checks": [
"entra_pim_privileged_role_administrator_approval_required"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2571,7 +2640,9 @@
{
"Id": "6.3.2",
"Description": "Outlook on the web (OWA) mailbox policies include two settings that control personal account integration in Outlook. PersonalAccountsEnabled controls whether users can add personal email accounts (e.g., Outlook.com, Gmail, Yahoo) in the new Outlook for Windows. PersonalAccountCalendarsEnabled controls whether users can connect personal Outlook.com or Google calendars in Outlook on the web. Neither setting applies to classic Outlook for Windows, Outlook for Mac, or Outlook mobile apps. The recommended state for the default OWA Mailbox Policy is: - PersonalAccountsEnabled is set to False - PersonalAccountCalendarsEnabled is set to False",
"Checks": [],
"Checks": [
"exchange_owa_mailbox_policy_personal_accounts_disabled"
],
"Attributes": [
{
"Section": "6 Exchange admin center",
@@ -2692,7 +2763,9 @@
{
"Id": "6.5.5",
"Description": "Direct Send is a method used to send emails directly to an Exchange Online customer's hosted mailboxes from on-premises devices, applications, or third-party cloud services using the customer's own accepted domain. This method does not require any form of authentication because, by its nature, it mimics incoming anonymous emails from the internet, apart from the sender domain. The recommended state is to configure RejectDirectSend to True.",
"Checks": [],
"Checks": [
"exchange_organization_reject_direct_send_enabled"
],
"Attributes": [
{
"Section": "6 Exchange admin center",
@@ -3088,7 +3161,9 @@
{
"Id": "8.2.4",
"Description": "This setting controls the organization's external access with Teams \"trial-only\" tenants. These are tenants that don't have any purchased seats. When set to Blocked, users from these trial-only tenants aren't able to search and contact your users via chats, Teams calls, and meetings (using the users' authenticated identities) and your users aren't able to reach users in these trial-only tenants. Users from the trial-only tenant are also removed from existing chats. The recommended state for People in my organization can communicate with accounts in trial Teams tenant is Off.",
"Checks": [],
"Checks": [
"teams_external_access_trial_tenants_blocked"
],
"Attributes": [
{
"Section": "8 Microsoft Teams admin center",
@@ -402,6 +402,39 @@ class M365PowerShell(PowerShellSession):
"Get-MalwareFilterPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_eop_protection_policy_rule(self) -> dict:
"""
Get Exchange Online Protection (EOP) preset security policy rules.
Returns:
dict: EOP protection policy rules in JSON format.
"""
return self.execute(
"Get-EOPProtectionPolicyRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_atp_protection_policy_rule(self) -> dict:
"""
Get Defender for Office 365 (ATP) preset security policy rules.
Returns:
dict: ATP protection policy rules in JSON format.
"""
return self.execute(
"Get-ATPProtectionPolicyRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_email_tenant_settings(self) -> dict:
"""
Get Defender email tenant settings.
Returns:
dict: Email tenant settings (e.g. EnablePriorityAccountProtection).
"""
return self.execute(
"Get-EmailTenantSettings | ConvertTo-Json -Depth 10", json_parse=True
)
def get_malware_filter_rule(self) -> dict:
"""
Get Defender Malware Filter Rule.
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "admincenter_forms_phishing_protection_enabled",
"CheckTitle": "Microsoft Forms internal phishing protection is enabled",
"CheckType": [],
"ServiceName": "admincenter",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "The Microsoft Forms organization settings should have **internal phishing protection** (**isInOrgFormsPhishingScanEnabled**) enabled. This scans forms for common phishing keywords and blocks the collection of responses while suspicious content is present.",
"Risk": "Without internal **phishing** protection, malicious users (internal or compromised accounts) can use **Microsoft Forms** to create convincing phishing forms that harvest credentials or sensitive information from other employees.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/microsoft-forms/administrator-settings-microsoft-forms"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft 365 admin center at https://admin.microsoft.com/\n2. Go to **Settings** > **Org settings** > **Services** > **Microsoft Forms**\n3. Under **Phishing protection**, check **Add internal phishing protection**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable internal phishing protection for Microsoft Forms so forms are automatically scanned for phishing keywords.",
"Url": "https://hub.prowler.com/check/admincenter_forms_phishing_protection_enabled"
}
},
"Categories": [
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,44 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.admincenter.admincenter_client import (
admincenter_client,
)
class admincenter_forms_phishing_protection_enabled(Check):
"""Check if Microsoft Forms internal phishing protection is enabled.
The Microsoft Forms org settings should have internal phishing protection
(``isInOrgFormsPhishingScanEnabled``) enabled so forms are scanned for phishing
keywords.
- PASS: Microsoft Forms internal phishing protection is enabled.
- FAIL: Microsoft Forms internal phishing protection is disabled.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = admincenter_client.forms_settings
if not settings:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=settings,
resource_name="Microsoft Forms Settings",
resource_id="formsSettings",
)
report.status = "FAIL"
report.status_extended = (
"Microsoft Forms internal phishing protection is not enabled."
)
if settings.in_org_forms_phishing_scan_enabled:
report.status = "PASS"
report.status_extended = (
"Microsoft Forms internal phishing protection is enabled."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "admincenter_office_store_apps_restricted",
"CheckTitle": "Users cannot access the Office Store or start trials",
"CheckType": [],
"ServiceName": "admincenter",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "The org-wide apps and services settings should disable **Let users access the Office Store** (**isOfficeStoreEnabled**) and **Let users start trials on behalf of your organization** (**isAppAndServicesTrialEnabled**). This prevents users from installing arbitrary store add-ins or provisioning trial services without administrative oversight.",
"Risk": "Allowing users to install **Office Store** add-ins or start service trials introduces unvetted third-party code and services into the tenant, which can access user data (mail, files) and expand the attack surface without administrator review.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/microsoft-365/admin/misc/user-owned-apps-and-services"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft 365 admin center at https://admin.microsoft.com/\n2. Go to **Settings** > **Org settings** > **Services** > **User owned apps and services**\n3. Uncheck **Let users access the Office Store**\n4. Uncheck **Let users start trials on behalf of your organization**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable Office Store access and organization trials so that only administrators can introduce add-ins and services into the tenant.",
"Url": "https://hub.prowler.com/check/admincenter_office_store_apps_restricted"
}
},
"Categories": [
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,47 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.admincenter.admincenter_client import (
admincenter_client,
)
class admincenter_office_store_apps_restricted(Check):
"""Check if users are restricted from accessing the Office Store and trials.
The org-wide apps and services settings should have both **Office Store access**
(``isOfficeStoreEnabled``) and **trials on behalf of the organization**
(``isAppAndServicesTrialEnabled``) disabled.
- PASS: Both Office Store access and trials are disabled.
- FAIL: Office Store access and/or trials are enabled.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = admincenter_client.apps_and_services_settings
if not settings:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=settings,
resource_name="Apps and Services Settings",
resource_id="appsAndServicesSettings",
)
report.status = "FAIL"
report.status_extended = (
"Users are allowed to access the Office Store and/or start trials."
)
if (
not settings.office_store_enabled
and not settings.app_and_services_trial_enabled
):
report.status = "PASS"
report.status_extended = (
"Users are not allowed to access the Office Store or start trials."
)
findings.append(report)
return findings
@@ -1,4 +1,5 @@
import asyncio
import json
from typing import List, Optional
from pydantic.v1 import BaseModel
@@ -14,10 +15,12 @@ class AdminCenter(M365Service):
self.organization_config = None
self.sharing_policy = None
self.mailbox_policies = []
if self.powershell:
if self.powershell.connect_exchange_online():
self.organization_config = self._get_organization_config()
self.sharing_policy = self._get_sharing_policy()
self.mailbox_policies = self._get_mailbox_policy()
self.powershell.close()
created_loop = False
@@ -46,12 +49,16 @@ class AdminCenter(M365Service):
self._get_directory_roles(),
self._get_groups(),
self._get_password_policy(),
self._get_apps_and_services_settings(),
self._get_forms_settings(),
)
)
self.directory_roles = attributes[0]
self.groups = attributes[1]
self.password_policy = attributes[2]
self.apps_and_services_settings = attributes[3]
self.forms_settings = attributes[4]
if created_loop:
asyncio.set_event_loop(None)
@@ -69,6 +76,9 @@ class AdminCenter(M365Service):
customer_lockbox_enabled=organization_configuration.get(
"CustomerLockboxEnabled", False
),
bookings_enabled=organization_configuration.get(
"BookingsEnabled", True
),
)
except Exception as error:
logger.error(
@@ -76,6 +86,31 @@ class AdminCenter(M365Service):
)
return organization_config
def _get_mailbox_policy(self):
logger.info("Microsoft365 - Getting OWA mailbox policy configuration...")
mailbox_policies = []
try:
policies_data = self.powershell.get_mailbox_policy()
if policies_data:
if isinstance(policies_data, dict):
policies_data = [policies_data]
for policy in policies_data:
if policy:
mailbox_policies.append(
OwaMailboxPolicy(
id=policy.get("Id", ""),
is_default=policy.get("IsDefault", False),
bookings_mailbox_creation_enabled=policy.get(
"BookingsMailboxCreationEnabled", True
),
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return mailbox_policies
def _get_sharing_policy(self):
logger.info("M365 - Getting sharing policy...")
sharing_policy = None
@@ -225,6 +260,71 @@ class AdminCenter(M365Service):
)
return password_policy
async def _get_apps_and_services_settings(self):
"""Retrieve the org-wide apps and services settings (beta Graph).
Fetches ``admin/appsAndServices/settings`` from the beta Graph endpoint to
read whether users can access the Office Store and start trials.
Returns:
Optional[AppsAndServicesSettings]: The parsed settings, or None on error.
"""
logger.info("M365 - Getting apps and services settings...")
settings = None
try:
builder = self.client.admin.with_url(
"https://graph.microsoft.com/beta/admin/appsAndServices/settings"
)
request_info = builder.to_get_request_information()
response = await self.client.request_adapter.send_primitive_async(
request_info, "bytes", {}
)
if response:
data = json.loads(response)
settings = AppsAndServicesSettings(
office_store_enabled=data.get("isOfficeStoreEnabled", True),
app_and_services_trial_enabled=data.get(
"isAppAndServicesTrialEnabled", True
),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return settings
async def _get_forms_settings(self):
"""Retrieve the Microsoft Forms org settings (beta Graph).
Fetches ``admin/forms/settings`` from the beta Graph endpoint to read whether
internal phishing protection is enabled.
Returns:
Optional[FormsSettings]: The parsed settings, or None on error.
"""
logger.info("M365 - Getting Microsoft Forms settings...")
settings = None
try:
builder = self.client.admin.with_url(
"https://graph.microsoft.com/beta/admin/forms/settings"
)
request_info = builder.to_get_request_information()
response = await self.client.request_adapter.send_primitive_async(
request_info, "bytes", {}
)
if response:
data = json.loads(response)
settings = FormsSettings(
in_org_forms_phishing_scan_enabled=data.get(
"isInOrgFormsPhishingScanEnabled", False
),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return settings
class User(BaseModel):
id: str
@@ -255,9 +355,25 @@ class Organization(BaseModel):
name: str
guid: str
customer_lockbox_enabled: bool
bookings_enabled: bool = True
class OwaMailboxPolicy(BaseModel):
id: str
is_default: bool = False
bookings_mailbox_creation_enabled: bool = True
class SharingPolicy(BaseModel):
name: str
guid: str
enabled: bool
class AppsAndServicesSettings(BaseModel):
office_store_enabled: bool = True
app_and_services_trial_enabled: bool = True
class FormsSettings(BaseModel):
in_org_forms_phishing_scan_enabled: bool = False
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "admincenter_shared_bookings_disabled",
"CheckTitle": "Microsoft Shared Bookings is disabled",
"CheckType": [],
"ServiceName": "admincenter",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Microsoft Bookings (Shared Bookings) should be turned off unless the organization has a business need for it. Bookings lets users create public booking pages so customers can schedule time with staff. It is considered compliant when it is disabled at the tenant level (**BookingsEnabled**) or when the default OWA mailbox policy blocks Bookings mailbox creation (**BookingsMailboxCreationEnabled**).",
"Risk": "**Bookings** pages are internet-facing and can expose staff names, email addresses, and availability, and may allow uncontrolled creation of Bookings mailboxes. Leaving Bookings enabled without a business need increases the organization's external attack surface and information disclosure risk.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/microsoft-365/bookings/turn-bookings-on-or-off"
],
"Remediation": {
"Code": {
"CLI": "Set-OrganizationConfig -BookingsEnabled $false",
"NativeIaC": "",
"Other": "1. Connect to Exchange Online PowerShell using Connect-ExchangeOnline\n2. To disable at the tenant level, run: Set-OrganizationConfig -BookingsEnabled $false\n3. Alternatively, disable in the default OWA mailbox policy: Set-OwaMailboxPolicy -Identity OwaMailboxPolicy-Default -BookingsMailboxCreationEnabled $false",
"Terraform": ""
},
"Recommendation": {
"Text": "Turn off Shared Bookings at the tenant level or in the default OWA mailbox policy unless there is a documented business need, in which case restrict who can create Bookings mailboxes.",
"Url": "https://hub.prowler.com/check/admincenter_shared_bookings_disabled"
}
},
"Categories": [
"internet-exposed",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,71 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.admincenter.admincenter_client import (
admincenter_client,
)
class admincenter_shared_bookings_disabled(Check):
"""Check if Microsoft Bookings (Shared Bookings) is disabled.
Bookings is considered disabled and compliant when either it is turned off at the
tenant level (OrganizationConfig BookingsEnabled) or the default OWA mailbox
policy prevents creation of Bookings mailboxes (BookingsMailboxCreationEnabled).
- PASS: Bookings is disabled at the tenant level or in the default OWA mailbox
policy.
- FAIL: Bookings is enabled at the tenant level and allowed by the default OWA
mailbox policy.
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the check for Shared Bookings.
Returns:
List[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
organization_config = admincenter_client.organization_config
if not organization_config:
return findings
default_policy = next(
(
policy
for policy in admincenter_client.mailbox_policies
if policy and policy.is_default
),
None,
)
report = CheckReportM365(
metadata=self.metadata(),
resource=organization_config,
resource_name=organization_config.name,
resource_id=organization_config.guid,
)
report.status = "FAIL"
report.status_extended = (
"Shared Bookings is enabled at the tenant level and in the default OWA "
"mailbox policy."
)
tenant_disabled = not organization_config.bookings_enabled
policy_disabled = bool(
default_policy and not default_policy.bookings_mailbox_creation_enabled
)
if tenant_disabled or policy_disabled:
report.status = "PASS"
if tenant_disabled:
report.status_extended = (
"Shared Bookings is disabled at the tenant level."
)
else:
report.status_extended = (
"Shared Bookings is disabled in the default OWA mailbox policy."
)
findings.append(report)
return findings
@@ -10,7 +10,7 @@
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "**Microsoft Defender for Office 365 connection filter policy** safe list setting is evaluated. When enabled, mail from Microsoft-managed IPs skips spam filtering and some sender authentication. The finding indicates whether this implicit bypass is turned off.",
"Risk": "With the safe list on, inbound mail can bypass SPF/DKIM/DMARC and spam heuristics, allowing spoofed or phishing messages to reach inboxes. This risks credential theft (confidentiality), enables account takeover and tampering (integrity), and may lead to malware-driven outages (availability).",
"Risk": "With the safe list on, inbound mail can bypass SPF/DKIM/DMARC and spam heuristics, allowing spoofed or **phishing** messages to reach inboxes. This risks credential theft (confidentiality), enables account takeover and tampering (integrity), and may lead to malware-driven outages (availability).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure",
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "defender_priority_account_protection_enabled",
"CheckTitle": "Priority account protection is enabled",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Priority account protection applies enhanced monitoring and protection to high-value accounts. The tenant-level flag **EnablePriorityAccountProtection** (from Get-EmailTenantSettings) should be enabled. This check evaluates the tenant-level enablement flag; tagging priority accounts and configuring alert policies must be verified separately.",
"Risk": "Without **priority account** protection, high-value targets such as executives receive the same protection as standard users, despite being far more likely to be targeted by **phishing** and **business email compromise**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/defender-office-365/priority-accounts-security-recommendations"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Defender at https://security.microsoft.com/\n2. Go to **System** > **Settings** > **Email & collaboration** > **Priority account protection**\n3. Set **Priority account protection** to **On**\n4. Tag priority accounts and configure the associated alert policies",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable priority account protection, tag your high-value accounts as priority accounts, and configure the associated alert policies for enhanced monitoring.",
"Url": "https://hub.prowler.com/check/defender_priority_account_protection_enabled"
}
},
"Categories": [
"email-security",
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Covers the tenant-level EnablePriorityAccountProtection flag only; alert-policy verification (Get-ProtectionAlert) requires a Security & Compliance PowerShell session that Prowler does not currently establish."
}
@@ -0,0 +1,46 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.defender.defender_client import defender_client
class defender_priority_account_protection_enabled(Check):
"""Check if priority account protection is enabled.
Priority account protection applies enhanced monitoring and protection to
high-value accounts. Its tenant-level flag ``EnablePriorityAccountProtection``
(from Get-EmailTenantSettings) should be enabled.
Note: This check covers the tenant-level enablement flag only. The full control
also requires priority accounts to be tagged and alert policies to be configured,
which must be verified manually.
- PASS: Priority account protection is enabled at the tenant level.
- FAIL: Priority account protection is disabled at the tenant level.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = defender_client.email_tenant_settings
if not settings:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=settings,
resource_name="Email Tenant Settings",
resource_id="emailTenantSettings",
)
report.status = "FAIL"
report.status_extended = (
"Priority account protection is not enabled at the tenant level."
)
if settings.priority_account_protection_enabled:
report.status = "PASS"
report.status_extended = (
"Priority account protection is enabled at the tenant level."
)
findings.append(report)
return findings
@@ -59,6 +59,9 @@ class Defender(M365Service):
self.safe_links_policies = {}
self.safe_links_rules = {}
self.teams_protection_policy = None
self.eop_protection_policy_rules = []
self.atp_protection_policy_rules = []
self.email_tenant_settings = None
if self.powershell:
if self.powershell.connect_exchange_online():
self.malware_policies = self._get_malware_filter_policy()
@@ -80,8 +83,88 @@ class Defender(M365Service):
self.safe_links_policies = self._get_safe_links_policy()
self.safe_links_rules = self._get_safe_links_rule()
self.teams_protection_policy = self._get_teams_protection_policy()
self.eop_protection_policy_rules = (
self._get_eop_protection_policy_rules()
)
self.atp_protection_policy_rules = (
self._get_atp_protection_policy_rules()
)
self.email_tenant_settings = self._get_email_tenant_settings()
self.powershell.close()
def _parse_protection_policy_rules(self, rules_data):
"""Parse preset security policy rules into PresetSecurityPolicyRule models."""
rules = []
if not rules_data:
return rules
if isinstance(rules_data, dict):
rules_data = [rules_data]
for rule in rules_data:
if rule:
rules.append(
PresetSecurityPolicyRule(
name=rule.get("Name", rule.get("Identity", "")),
state=rule.get("State", ""),
sent_to=self._normalize_list(rule.get("SentTo")),
sent_to_member_of=self._normalize_list(
rule.get("SentToMemberOf")
),
recipient_domain_is=self._normalize_list(
rule.get("RecipientDomainIs")
),
)
)
return rules
@staticmethod
def _normalize_list(value):
"""Normalize a PowerShell scalar/list/None value into a list."""
if value is None:
return []
if isinstance(value, list):
return value
return [value]
def _get_eop_protection_policy_rules(self):
logger.info("M365 - Getting Defender EOP protection policy rules...")
try:
return self._parse_protection_policy_rules(
self.powershell.get_eop_protection_policy_rule()
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return []
def _get_atp_protection_policy_rules(self):
logger.info("M365 - Getting Defender ATP protection policy rules...")
try:
return self._parse_protection_policy_rules(
self.powershell.get_atp_protection_policy_rule()
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return []
def _get_email_tenant_settings(self):
logger.info("M365 - Getting Defender email tenant settings...")
try:
data = self.powershell.get_email_tenant_settings()
if data:
return EmailTenantSettings(
priority_account_protection_enabled=data.get(
"EnablePriorityAccountProtection", False
),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return None
def _get_malware_filter_policy(self):
logger.info("M365 - Getting Defender malware filter policy...")
malware_policies = []
@@ -826,3 +909,24 @@ class TeamsProtectionPolicy(BaseModel):
identity: str
zap_enabled: bool
class PresetSecurityPolicyRule(BaseModel):
"""Model for a preset security policy rule (EOP or ATP)."""
name: str = ""
state: str = ""
sent_to: list = []
sent_to_member_of: list = []
recipient_domain_is: list = []
@property
def has_recipients(self) -> bool:
"""True if the rule targets at least one recipient scope."""
return bool(self.sent_to or self.sent_to_member_of or self.recipient_domain_is)
class EmailTenantSettings(BaseModel):
"""Model for Defender email tenant settings."""
priority_account_protection_enabled: bool = False
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "defender_strict_preset_security_policy_enabled",
"CheckTitle": "Strict Preset Security Policy is enabled",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "The **Strict Preset Security Policy** applies Microsoft's recommended strict protection settings and should be enabled for both **Exchange Online Protection** (anti-phishing, anti-spam, anti-malware) and **Defender for Office 365** (Safe Attachments, Safe Links). The rules are exposed via Get-EOPProtectionPolicyRule and Get-ATPProtectionPolicyRule.",
"Risk": "Without the **Strict Preset Security Policy** enabled, mailboxes rely on weaker default or custom protection settings, increasing exposure to **phishing**, malware, and malicious links and attachments.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/defender-office-365/preset-security-policies"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Defender at https://security.microsoft.com/\n2. Go to **Email & collaboration** > **Policies & rules** > **Threat policies** > **Preset security policies**\n3. Turn on the **Strict protection** preset and assign it to the appropriate users, groups, or domains (including priority accounts)",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the Strict Preset Security Policy for both Exchange Online Protection and Defender for Office 365 and assign it to your users and priority accounts.",
"Url": "https://hub.prowler.com/check/defender_strict_preset_security_policy_enabled"
}
},
"Categories": [
"email-security",
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,66 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.defender.defender_client import defender_client
STRICT_PRESET_NAME = "Strict Preset Security Policy"
class defender_strict_preset_security_policy_enabled(Check):
"""Check if the Strict Preset Security Policy is enabled for EOP and Defender.
The Strict Preset Security Policy applies Microsoft's recommended strict
protection settings. It should be enabled for both Exchange Online Protection
(anti-phishing, anti-spam, anti-malware) and Defender for Office 365 (Safe
Attachments, Safe Links).
- PASS: The Strict Preset Security Policy is enabled for both EOP and Defender.
- FAIL: The Strict Preset Security Policy is not enabled for EOP and/or Defender.
"""
def _has_enabled_strict_preset(self, rules) -> bool:
return any(
rule.name == STRICT_PRESET_NAME
and rule.state == "Enabled"
and rule.has_recipients
for rule in rules
)
def execute(self) -> List[CheckReportM365]:
findings = []
report = CheckReportM365(
metadata=self.metadata(),
resource={
"eop": [
rule.dict() for rule in defender_client.eop_protection_policy_rules
],
"atp": [
rule.dict() for rule in defender_client.atp_protection_policy_rules
],
},
resource_name="Strict Preset Security Policy",
resource_id="strictPresetSecurityPolicy",
)
eop_enabled = self._has_enabled_strict_preset(
defender_client.eop_protection_policy_rules
)
atp_enabled = self._has_enabled_strict_preset(
defender_client.atp_protection_policy_rules
)
if eop_enabled and atp_enabled:
report.status = "PASS"
report.status_extended = (
"The Strict Preset Security Policy is enabled for both Exchange "
"Online Protection and Defender for Office 365."
)
else:
report.status = "FAIL"
report.status_extended = (
"The Strict Preset Security Policy is not enabled for both Exchange "
"Online Protection and Defender for Office 365."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_access_review_guest_users_configured",
"CheckTitle": "Access review for guest users is configured",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "An **access review** scoped to **guest users** should exist and be active so that external guest access is periodically recertified by reviewers. Requires a Microsoft Entra ID P2 license.",
"Risk": "Without recurring access reviews for guests, external accounts accumulate over time and retain access long after it is needed, expanding the attack surface and the risk of orphaned or abused guest accounts.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/id-governance/create-access-review"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **ID Governance** > **Access reviews** > **New access review**\n3. Set the scope to **Guest users only**\n4. Assign at least one non-guest reviewer, enable mail notifications and reminders\n5. Set a recurring schedule and create the review",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a recurring access review scoped to guest users with assigned reviewers so guest access is periodically recertified.",
"Url": "https://hub.prowler.com/check/entra_access_review_guest_users_configured"
}
},
"Categories": [
"identity-access",
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Requires Microsoft Entra ID P2."
}
@@ -0,0 +1,64 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
ACTIVE_STATUSES = {"InProgress"}
class entra_access_review_guest_users_configured(Check):
"""Check if an access review for guest users is configured and fail-closed.
An access review scoped to guest users should exist, be active, and be
fail-closed: if reviewers do not respond, access is removed
(``defaultDecision`` = Deny with ``autoApplyDecisionsEnabled``), with mail
notifications and reminders enabled.
- PASS: An active, fail-closed access review scoped to guest users exists.
- FAIL: No such access review exists (missing, inactive, or not fail-closed).
"""
def _is_fail_closed(self, definition) -> bool:
return (
definition.default_decision == "Deny"
and definition.auto_apply_enabled
and definition.mail_notifications_enabled
and definition.reminders_enabled
)
def execute(self) -> List[CheckReportM365]:
findings = []
definitions = entra_client.access_review_definitions
report = CheckReportM365(
metadata=self.metadata(),
resource=definitions if definitions else {},
resource_name="Access Review Definitions",
resource_id="accessReviewDefinitions",
)
report.status = "FAIL"
report.status_extended = (
"No active fail-closed access review scoped to guest users is configured."
)
for definition in definitions:
if (
definition.status in ACTIVE_STATUSES
and "guest" in definition.scope_query.lower()
and self._is_fail_closed(definition)
):
report = CheckReportM365(
metadata=self.metadata(),
resource=definition,
resource_name=definition.display_name or "Access Review",
resource_id=definition.id,
)
report.status = "PASS"
report.status_extended = (
f"Access review '{definition.display_name or definition.id}' for "
"guest users is active and fail-closed."
)
break
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_access_review_privileged_roles_configured",
"CheckTitle": "Access review for privileged roles is configured",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "An **access review** scoped to **privileged (PIM) directory roles** should exist and be active so privileged role assignments are periodically recertified by reviewers. Requires a Microsoft Entra ID P2 license.",
"Risk": "Without recurring access reviews of privileged roles, standing privileged assignments accumulate and persist beyond their need, increasing the risk of privilege misuse and expanding the impact of a compromised administrator account.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-create-roles-and-resource-roles-review"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **ID Governance** > **Privileged Identity Management** > **Microsoft Entra Roles** > **Access reviews** > **New**\n3. Scope the review to the privileged roles, set the assignment type to Eligible and Active\n4. Assign reviewers, enable notifications, and set a recurring schedule\n5. Create the review",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a recurring access review scoped to privileged roles with assigned reviewers so privileged role assignments are periodically recertified.",
"Url": "https://hub.prowler.com/check/entra_access_review_privileged_roles_configured"
}
},
"Categories": [
"identity-access",
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Requires Microsoft Entra ID P2."
}
@@ -0,0 +1,76 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
ACTIVE_STATUSES = {"InProgress"}
# Markers that indicate the review targets directory role assignments (PIM roles).
PRIVILEGED_SCOPE_MARKERS = ("roledefinition", "rolemanagement", "roleassignment")
class entra_access_review_privileged_roles_configured(Check):
"""Check if an access review for privileged roles is configured and fail-closed.
An access review scoped to privileged (PIM) directory roles should exist, be
active, and be fail-closed: if reviewers do not respond, access is removed
(``defaultDecision`` = Deny with ``autoApplyDecisionsEnabled``), with mail
notifications and reminders enabled.
- PASS: An active, fail-closed access review scoped to privileged roles exists.
- FAIL: No such access review exists (missing, inactive, or not fail-closed).
"""
def _targets_privileged_roles(self, definition) -> bool:
# For PIM role reviews the role reference lives in the resource scopes.
queries = [definition.scope_query] + definition.resource_scope_queries
return any(
marker in query.lower()
for query in queries
for marker in PRIVILEGED_SCOPE_MARKERS
)
def _is_fail_closed(self, definition) -> bool:
return (
definition.default_decision == "Deny"
and definition.auto_apply_enabled
and definition.mail_notifications_enabled
and definition.reminders_enabled
)
def execute(self) -> List[CheckReportM365]:
findings = []
definitions = entra_client.access_review_definitions
report = CheckReportM365(
metadata=self.metadata(),
resource=definitions if definitions else {},
resource_name="Access Review Definitions",
resource_id="accessReviewDefinitions",
)
report.status = "FAIL"
report.status_extended = (
"No active fail-closed access review scoped to privileged roles is "
"configured."
)
for definition in definitions:
if (
definition.status in ACTIVE_STATUSES
and self._targets_privileged_roles(definition)
and self._is_fail_closed(definition)
):
report = CheckReportM365(
metadata=self.metadata(),
resource=definition,
resource_name=definition.display_name or "Access Review",
resource_id=definition.id,
)
report.status = "PASS"
report.status_extended = (
f"Access review '{definition.display_name or definition.id}' for "
"privileged roles is active and fail-closed."
)
break
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_authentication_method_authenticator_companion_app_disabled",
"CheckTitle": "Microsoft Authenticator on companion applications is disabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The Microsoft Authenticator method should have **Authenticator Lite on companion applications** (**featureSettings.companionAppAllowedState**) set to **disabled**. Authenticator Lite embeds a subset of Authenticator functionality into companion apps such as Outlook mobile, expanding where approvals can occur.",
"Risk": "Allowing **MFA** approvals from **companion applications** broadens the set of apps and devices that can approve sign-ins, increasing the attack surface for MFA fatigue and reducing the assurance provided by the dedicated Authenticator app.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-mfa-authenticator-lite"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Policies** > **Microsoft Authenticator**\n3. Select **Configure**\n4. Set **Microsoft Authenticator on companion applications** to **Disabled**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable Microsoft Authenticator on companion applications (Authenticator Lite) so that MFA approvals require the full Microsoft Authenticator app.",
"Url": "https://hub.prowler.com/check/entra_authentication_method_authenticator_companion_app_disabled"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,43 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_authentication_method_authenticator_companion_app_disabled(Check):
"""Check if Microsoft Authenticator on companion applications is disabled.
Authenticator Lite embeds a subset of Microsoft Authenticator functionality into
companion applications such as Outlook mobile. The
``featureSettings.companionAppAllowedState`` should be disabled so MFA is bound to
the full Authenticator app.
- PASS: Microsoft Authenticator on companion applications is disabled.
- FAIL: Microsoft Authenticator on companion applications is not disabled.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = entra_client.authentication_methods_policy_settings
if not settings:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=settings,
resource_name="Microsoft Authenticator Method",
resource_id="microsoftAuthenticator",
)
report.status = "FAIL"
report.status_extended = (
"Microsoft Authenticator on companion applications is not disabled."
)
if settings.authenticator_companion_app_state == "disabled":
report.status = "PASS"
report.status_extended = (
"Microsoft Authenticator on companion applications is disabled."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_authentication_method_authenticator_show_context",
"CheckTitle": "Microsoft Authenticator shows application name and geographic location",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The Microsoft Authenticator authentication method should have the **Show application name** (**displayAppInformationRequiredState**) and **Show geographic location** (**displayLocationInformationRequiredState**) feature settings enabled. This adds context to push and passwordless notifications so users can detect and reject fraudulent approval requests.",
"Risk": "Without application-name and location context, users approving **MFA** push notifications cannot tell a legitimate sign-in from an attacker-initiated one, making the tenant more vulnerable to MFA fatigue and prompt-bombing attacks.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-mfa-additional-context"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Policies** > **Microsoft Authenticator**\n3. Select **Configure**\n4. Set **Show application name in push and passwordless notifications** to **Enabled**, Target **All users**\n5. Set **Show geographic location in push and passwordless notifications** to **Enabled**, Target **All users**\n6. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the application-name and geographic-location context settings for Microsoft Authenticator so users receive additional information to identify and reject fraudulent sign-in approvals.",
"Url": "https://hub.prowler.com/check/entra_authentication_method_authenticator_show_context"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,48 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_authentication_method_authenticator_show_context(Check):
"""Check if Microsoft Authenticator shows application name and geographic location.
The Microsoft Authenticator method should be enabled with the
``displayAppInformationRequiredState`` and
``displayLocationInformationRequiredState`` feature settings enabled, so users see
the app name and sign-in location context in push and passwordless notifications.
- PASS: Both application name and geographic location context are shown.
- FAIL: Application name and/or geographic location context is not shown.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = entra_client.authentication_methods_policy_settings
if not settings:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=settings,
resource_name="Microsoft Authenticator Method",
resource_id="microsoftAuthenticator",
)
report.status = "FAIL"
report.status_extended = (
"Microsoft Authenticator does not show both application name and "
"geographic location in notifications."
)
if (
settings.authenticator_display_app_information_state == "enabled"
and settings.authenticator_display_location_information_state == "enabled"
):
report.status = "PASS"
report.status_extended = (
"Microsoft Authenticator shows application name and geographic "
"location in notifications."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_authentication_method_email_otp_disabled",
"CheckTitle": "Email One-Time Passcode authentication method is disabled in the tenant",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra tenant's authentication methods policy should have the **Email OTP** authentication method disabled. Email one-time passcodes depend on the security of the recipient mailbox, which is typically a lower-assurance channel, and should not be used as a primary or fallback multi-factor authentication method for members.",
"Risk": "**Email OTP** is vulnerable to mailbox compromise and **phishing**. If an attacker gains access to a user's mailbox, they can intercept one-time passcodes and bypass **MFA**, gaining unauthorized access to the tenant.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/howto-authentication-email-otp"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Policies**\n3. Select **Email OTP** and set its status to **Disabled**, then click **Save**\n4. Ensure users have phishing-resistant MFA methods configured (e.g., FIDO2, Microsoft Authenticator)",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable the Email OTP authentication method and require phishing-resistant MFA methods such as FIDO2 security keys or Microsoft Authenticator across the tenant.",
"Url": "https://hub.prowler.com/check/entra_authentication_method_email_otp_disabled"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,51 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_authentication_method_email_otp_disabled(Check):
"""
Ensure that the Email One-Time Passcode (OTP) authentication method is disabled.
This check verifies that the tenant's authentication methods policy has the Email OTP
method disabled. Email OTP relies on the security of the mailbox, which is often a
lower-assurance channel and is unsuitable as a primary or fallback MFA method.
- PASS: Email OTP authentication method is disabled.
- FAIL: Email OTP authentication method is enabled.
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the Email OTP authentication method check.
Returns:
A list with a single report containing the result of the check.
"""
findings = []
configs = entra_client.authentication_method_configurations
email_config = configs.get("Email")
if email_config:
report = CheckReportM365(
metadata=self.metadata(),
resource=email_config,
resource_name="Email OTP Authentication Method",
resource_id=entra_client.tenant_domain,
)
if email_config.state == "enabled":
report.status = "FAIL"
report.status_extended = (
"Email OTP authentication method is enabled in the tenant."
)
else:
report.status = "PASS"
report.status_extended = (
"Email OTP authentication method is disabled in the tenant."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_authentication_method_system_preferred_mfa_enabled",
"CheckTitle": "System-preferred multifactor authentication is enabled for all users",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant authentication methods policy should have **system-preferred multifactor authentication** (**systemCredentialPreferences**) enabled and targeting **all users**. System-preferred MFA prompts users to authenticate with the most secure method they have registered.",
"Risk": "Without **system-preferred** **MFA**, users may default to weaker registered methods (such as SMS) instead of stronger ones (such as Authenticator push or passwordless), lowering the overall assurance of multifactor authentication.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-system-preferred-multifactor-authentication"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Settings**\n3. Set **System-preferred multifactor authentication** to **Enabled** and Target **All users**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable system-preferred multifactor authentication for all users so Entra prompts each user to sign in with the most secure method they have registered.",
"Url": "https://hub.prowler.com/check/entra_authentication_method_system_preferred_mfa_enabled"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,51 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
ALL_USERS_TARGETS = {"all_users", "all users"}
class entra_authentication_method_system_preferred_mfa_enabled(Check):
"""Check if system-preferred multifactor authentication is enabled for all users.
System-preferred MFA prompts users to sign in with the most secure method they
have registered. Its ``systemCredentialPreferences.state`` should be enabled and
target all users.
- PASS: System-preferred MFA is enabled and targets all users.
- FAIL: System-preferred MFA is disabled or does not target all users.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = entra_client.authentication_methods_policy_settings
if not settings:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=settings,
resource_name="System-preferred MFA",
resource_id="systemCredentialPreferences",
)
report.status = "FAIL"
report.status_extended = (
"System-preferred multifactor authentication is not enabled for all users."
)
targets = {
str(target).lower()
for target in settings.system_preferred_mfa_include_targets
}
if (
settings.system_preferred_mfa_state == "enabled"
and targets & ALL_USERS_TARGETS
):
report.status = "PASS"
report.status_extended = (
"System-preferred multifactor authentication is enabled for all users."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_authentication_transfer_blocked",
"CheckTitle": "Conditional Access policy blocks authentication transfer",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "At least one enabled Conditional Access policy should target the **authentication transfer** flow for **all users** and **all resources** and set the grant control to **Block**. Authentication transfer lets a user move an authenticated session from one device to another (for example, by scanning a QR code), which can be abused to move a session onto an attacker-controlled device.",
"Risk": "If authentication transfer is not blocked, an attacker who can present a transfer prompt (e.g., a malicious QR code) may capture an authenticated session and gain access to the victim's resources without needing their credentials or **MFA**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-conditions"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Conditional Access** > **Policies** > **New policy**\n3. Under **Users**, include **All users** (exclude only documented break-glass accounts)\n4. Under **Target resources**, select **All resources**\n5. Under **Conditions** > **Authentication flows**, enable and select **Authentication transfer**\n6. Under **Grant**, select **Block access**\n7. Set the policy to **On** and click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that blocks the authentication transfer flow for all users and all resources, excluding only documented break-glass accounts.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_authentication_transfer_blocked"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,81 @@
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,
TransferMethod,
)
class entra_conditional_access_policy_authentication_transfer_blocked(Check):
"""Check if at least one Conditional Access policy blocks authentication transfer.
This check ensures that at least one enabled Conditional Access policy targets
the authentication transfer flow and blocks access, preventing an authenticated
session from being seamlessly transferred to another (potentially attacker
controlled) device.
- PASS: An enabled Conditional Access policy blocks authentication transfer.
- FAIL: No Conditional Access policy restricts authentication transfer.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check to verify authentication transfer is blocked by a Conditional Access policy.
Returns:
A list of reports containing the result of the check.
"""
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 blocks authentication transfer."
)
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 not policy.conditions.authentication_flows:
continue
if (
TransferMethod.AUTHENTICATION_TRANSFER
not in policy.conditions.authentication_flows.transfer_methods
):
continue
if (
ConditionalAccessGrantControl.BLOCK
in policy.grant_controls.built_in_controls
):
report = CheckReportM365(
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 authentication transfer but does not block it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks authentication transfer."
break
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_block_high_medium_sign_in_risk",
"CheckTitle": "Conditional Access policy blocks high and medium sign-in risk",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "At least one enabled Conditional Access policy should target **all users** and **all resources** with the **sign-in risk** condition set to **High** and **Medium** and the grant control set to **Block**. Microsoft Entra ID Protection evaluates sign-in risk in real time; blocking risky sign-ins prevents access from sessions that are likely not performed by the legitimate account owner.",
"Risk": "Without a **sign-in risk** based **Conditional Access** policy, sign-ins flagged as high or medium risk (indicative of compromised credentials, anomalous locations, or malicious IPs) are allowed, giving attackers a path to access resources with stolen credentials.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/id-protection/concept-identity-protection-policies",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-conditions#sign-in-risk"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Conditional Access** > **Policies** > **New policy**\n3. Under **Users**, include **All users** (exclude only documented break-glass accounts)\n4. Under **Target resources**, select **All resources**\n5. Under **Conditions** > **Sign-in risk**, select **High** and **Medium**\n6. Under **Grant**, select **Block access**\n7. Set the policy to **On** and click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a risk-based Conditional Access policy that blocks high and medium sign-in risk for all users and all resources, excluding only documented break-glass accounts. Microsoft recommends configuring risk-based policies in Conditional Access rather than in the legacy Identity Protection policies.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_block_high_medium_sign_in_risk"
}
},
"Categories": [
"identity-access",
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,80 @@
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,
RiskLevel,
)
class entra_conditional_access_policy_block_high_medium_sign_in_risk(Check):
"""Check if a Conditional Access policy blocks high and medium sign-in risk.
This check ensures that at least one enabled Conditional Access policy targets
all users and all resources with the sign-in risk condition set to High and
Medium, and blocks access, mitigating risky sign-in attempts detected by
Microsoft Entra ID Protection.
- PASS: An enabled Conditional Access policy blocks high and medium sign-in risk.
- FAIL: No Conditional Access policy blocks high and medium sign-in risk.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check to verify high and medium sign-in risk is blocked.
Returns:
A list of reports containing the result of the check.
"""
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 blocks high and medium sign-in risk."
)
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 policy.conditions.application_conditions.excluded_applications:
continue
if not {RiskLevel.HIGH, RiskLevel.MEDIUM}.issubset(
set(policy.conditions.sign_in_risk_levels)
):
continue
if (
ConditionalAccessGrantControl.BLOCK
in policy.grant_controls.built_in_controls
):
report = CheckReportM365(
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 high and medium sign-in risk but does not block it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks high and medium sign-in risk."
break
findings.append(report)
return findings
@@ -10,7 +10,7 @@
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra **Conditional Access** policies can require **multifactor authentication (MFA)** for **device registration** operations.\n\nThis control ensures users must complete MFA before **registering or joining devices** to the directory, reducing the likelihood that compromised credentials can be used to register rogue devices.",
"Risk": "Without MFA for device registration, attackers with stolen credentials could register unauthorized devices into the directory, gain persistence, and bypass compliance-based Conditional Access protections that rely on trusted device state.",
"Risk": "Without **MFA** for device registration, attackers with stolen credentials could register unauthorized devices into the directory, gain persistence, and bypass compliance-based **Conditional Access** protections that rely on trusted device state.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-device-registration",
@@ -10,7 +10,7 @@
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Conditional Access policies scoped to **all users** and **all cloud applications** are evaluated to confirm the **Directory Synchronization Accounts** role is explicitly excluded. The Microsoft Entra Connect Sync Account does not support multifactor authentication, so it must be excluded from restrictive policies to maintain directory synchronization.",
"Risk": "If the Directory Synchronization Accounts role is not excluded from Conditional Access policies requiring MFA or blocking access, the Entra Connect Sync Account will be unable to authenticate. This breaks hybrid identity synchronization between on-premises Active Directory and Entra ID, potentially causing authentication failures and identity inconsistencies.",
"Risk": "If the Directory Synchronization Accounts role is not excluded from **Conditional Access** policies requiring **MFA** or blocking access, the Entra Connect Sync Account will be unable to authenticate. This breaks hybrid identity synchronization between **on-premises** Active Directory and Entra ID, potentially causing authentication failures and identity inconsistencies.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-all-users-mfa",
@@ -10,7 +10,7 @@
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra **Conditional Access** is verified to have at least one **enabled** policy that explicitly includes the **Azure DevOps** cloud application. Policies targeting **All** cloud apps do not satisfy this check because the goal is to verify that Azure DevOps has been deliberately considered.",
"Risk": "Without an explicit Conditional Access policy for Azure DevOps, organizations may rely on broad policies that do not account for Azure DevOps-specific access patterns such as CLI, IDE plug-ins, PAT-based workflows, source code access, build pipelines, secrets, and service connections.",
"Risk": "Without an explicit **Conditional Access** policy for Azure DevOps, organizations may rely on broad policies that do not account for Azure DevOps-specific access patterns such as CLI, IDE plug-ins, PAT-based workflows, source code access, build pipelines, secrets, and service connections.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/graph/api/resources/conditionalaccesspolicy?view=graph-rest-1.0",
@@ -10,7 +10,7 @@
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Every object identifier referenced by any Conditional Access policy under conditions.users (includeUsers, excludeUsers, includeGroups, excludeGroups, includeRoles, excludeRoles) must resolve to an existing Microsoft Entra object. This check audits all Conditional Access policies regardless of state and reports any whose user, group, or role references no longer resolve in the directory.",
"Risk": "When a user, group, or directory role referenced by a Conditional Access policy stops resolving (account or group deleted, role template removed), the reference becomes orphaned. include* references silently shrink the policy's enforcement scope; exclude* references can cause the policy to evaluate unexpectedly. This is a common root cause of MFA-not-applied incidents.",
"Risk": "When a user, group, or directory role referenced by a **Conditional Access** policy stops resolving (account or group deleted, role template removed), the reference becomes orphaned. include* references silently shrink the policy's enforcement scope; exclude* references can cause the policy to evaluate unexpectedly. This is a common root cause of **MFA**-not-applied incidents.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/graph/api/resources/conditionalaccesspolicy?view=graph-rest-1.0",
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_sign_in_frequency_all_users",
"CheckTitle": "Conditional Access policy enforces sign-in frequency for all users",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "At least one enabled Conditional Access policy should target **all users** and **all resources** and enforce a **sign-in frequency of 7 days or less**. Sign-in frequency defines how long an authenticated session remains valid before the user must reauthenticate. The Microsoft Entra default is a rolling 90-day window, which is too permissive.",
"Risk": "Long-lived sessions increase the window in which a stolen token or hijacked session can be used. Without an enforced reauthentication interval, a compromised session can remain valid for up to 90 days, extending an attacker's access.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Conditional Access** > **Policies** > **New policy**\n3. Under **Users**, include **All users** (exclude only documented break-glass accounts)\n4. Under **Target resources**, select **All resources**\n5. Under **Session**, enable **Sign-in frequency** and set it to **7 days** or less\n6. Set the policy to **On** and click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that enforces a sign-in frequency of 7 days or less for all users and all resources, excluding only documented break-glass accounts.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_sign_in_frequency_all_users"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,101 @@
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 (
ConditionalAccessPolicyState,
SignInFrequencyInterval,
SignInFrequencyType,
)
# Maximum allowed reauthentication window (CIS: 7 days or less).
MAX_SIGN_IN_FREQUENCY_DAYS = 7
MAX_SIGN_IN_FREQUENCY_HOURS = MAX_SIGN_IN_FREQUENCY_DAYS * 24
class entra_conditional_access_policy_sign_in_frequency_all_users(Check):
"""Check if a Conditional Access policy enforces sign-in frequency for all users.
This check ensures that at least one enabled Conditional Access policy targets
all users and all resources and enforces a sign-in frequency of 7 days or less,
limiting how long an authenticated session remains valid before reauthentication.
- PASS: An enabled Conditional Access policy enforces sign-in frequency of 7 days
or less for all users.
- FAIL: No Conditional Access policy enforces sign-in frequency of 7 days or less
for all users.
"""
def _is_within_limit(self, sign_in_frequency) -> bool:
"""Return True if the sign-in frequency is 7 days or less."""
if not sign_in_frequency or not sign_in_frequency.is_enabled:
return False
if sign_in_frequency.interval == SignInFrequencyInterval.EVERY_TIME:
return True
if sign_in_frequency.frequency is None:
return False
if sign_in_frequency.type == SignInFrequencyType.DAYS:
return sign_in_frequency.frequency <= MAX_SIGN_IN_FREQUENCY_DAYS
if sign_in_frequency.type == SignInFrequencyType.HOURS:
return sign_in_frequency.frequency <= MAX_SIGN_IN_FREQUENCY_HOURS
return False
def execute(self) -> list[CheckReportM365]:
"""Execute the check to verify sign-in frequency is enforced for all users.
Returns:
A list of reports containing the result of the check.
"""
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 enforces a sign-in frequency of 7 days or less for all 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
# A policy scoped to risky sign-ins/users does not enforce periodic
# reauthentication for all sessions, so it does not satisfy this control.
if (
policy.conditions.sign_in_risk_levels
or policy.conditions.user_risk_levels
):
continue
if not self._is_within_limit(policy.session_controls.sign_in_frequency):
continue
report = CheckReportM365(
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}' enforces sign-in frequency but is in report-only mode."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces a sign-in frequency of 7 days or less for all users."
break
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_token_protection_enforced",
"CheckTitle": "Conditional Access policy enforces Token Protection",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "At least one enabled Conditional Access policy should enforce **Token Protection** (the **secureSignInSession** session control) for targeted users. Token Protection binds sign-in session tokens (such as Primary Refresh Tokens) to the device, reducing the effectiveness of token theft and replay attacks.",
"Risk": "Without **Token Protection**, stolen sign-in tokens can be replayed from an attacker's device to impersonate the user and bypass authentication, including **MFA**, because the token is not cryptographically bound to the original device.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Conditional Access** > **Policies** > **New policy**\n3. Under **Users**, include the targeted users (exclude only documented break-glass accounts)\n4. Under **Target resources**, select the supported applications (e.g., Exchange Online, SharePoint Online)\n5. Under **Conditions** > **Device platforms**, include Windows\n6. Under **Session**, enable **Require token protection for sign-in sessions**\n7. Set the policy to **On** and click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that enforces Token Protection (secure sign-in session) for supported applications on Windows devices to mitigate token theft and replay.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_token_protection_enforced"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,99 @@
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 (
ClientAppType,
ConditionalAccessPolicyState,
)
# Applications that must be covered by the Token Protection policy.
EXCHANGE_ONLINE_APP_ID = "00000002-0000-0ff1-ce00-000000000000"
SHAREPOINT_ONLINE_APP_ID = "00000003-0000-0ff1-ce00-000000000000"
TEAMS_APP_ID = "cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"
REQUIRED_APP_IDS = {EXCHANGE_ONLINE_APP_ID, SHAREPOINT_ONLINE_APP_ID, TEAMS_APP_ID}
class entra_conditional_access_policy_token_protection_enforced(Check):
"""Check if a Conditional Access policy enforces Token Protection.
Token Protection is a Conditional Access session control that reduces token
replay attacks by requiring device-bound sign-in session tokens. At least one
enabled Conditional Access policy should enable the ``secureSignInSession``
session control for the supported applications (Exchange Online, SharePoint
Online, Teams), target the Windows platform, and apply to mobile apps and desktop
clients.
- PASS: An enabled Conditional Access policy enforces Token Protection for the
supported applications on Windows desktop/mobile clients.
- FAIL: No Conditional Access policy enforces Token Protection with the required
scope.
"""
def _applications_covered(self, included_applications) -> bool:
if "All" in included_applications:
return True
return REQUIRED_APP_IDS.issubset(set(included_applications))
def _windows_targeted(self, conditions) -> bool:
platform_conditions = conditions.platform_conditions
if not platform_conditions:
return False
return "windows" in platform_conditions.include_platforms
def _desktop_clients_targeted(self, conditions) -> bool:
client_app_types = conditions.client_app_types or []
return (
ClientAppType.ALL in client_app_types
or ClientAppType.MOBILE_APPS_AND_DESKTOP_CLIENTS in client_app_types
)
def execute(self) -> list[CheckReportM365]:
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 enforces Token Protection for the supported "
"applications on Windows desktop and mobile clients."
)
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if not policy.conditions.user_conditions.included_users:
continue
if not policy.session_controls.secure_sign_in_session_enabled:
continue
if not self._applications_covered(
policy.conditions.application_conditions.included_applications
):
continue
if not self._windows_targeted(policy.conditions):
continue
if not self._desktop_clients_targeted(policy.conditions):
continue
report = CheckReportM365(
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}' enables Token Protection but is in report-only mode."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces Token Protection for the supported applications on Windows desktop and mobile clients."
break
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_untrusted_locations_blocked",
"CheckTitle": "Conditional Access policy blocks access from untrusted locations",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "At least one enabled Conditional Access policy should target **all users** and **all resources**, include **all network locations**, exclude **trusted locations**, and set the grant control to **Block**. This blocks access originating from locations that are not explicitly trusted by the organization.",
"Risk": "Without a location-based block policy, users can authenticate from any geography or network, including regions where the organization has no presence, giving attackers operating from those locations a viable path to access resources with stolen credentials.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-assignment-network"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Conditional Access** > **Policies** > **New policy**\n3. Under **Users**, include **All users** (exclude only documented break-glass accounts)\n4. Under **Target resources**, select **All resources**\n5. Under **Network**, include **Any location** and exclude **All trusted locations** (or selected trusted named locations)\n6. Under **Grant**, select **Block access**\n7. Set the policy to **On** and click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that blocks access from all locations except trusted named locations, targeting all users and resources, excluding only documented break-glass accounts.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_untrusted_locations_blocked"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,97 @@
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,
)
class entra_conditional_access_policy_untrusted_locations_blocked(Check):
"""Check if a Conditional Access policy blocks access from untrusted locations.
At least one enabled Conditional Access policy should target all users and all
resources, include all network locations, exclude trusted locations, and block
access, so that access from untrusted locations is denied.
- PASS: An enabled Conditional Access policy blocks access from untrusted
locations.
- FAIL: No Conditional Access policy blocks access from untrusted locations.
"""
def _excludes_only_trusted(self, exclude_locations, trusted_location_ids) -> bool:
"""Return True if the excluded locations are all trusted."""
if not exclude_locations:
return False
if "AllTrusted" in exclude_locations:
return True
return all(
location_id in trusted_location_ids for location_id in exclude_locations
)
def execute(self) -> list[CheckReportM365]:
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 blocks access from untrusted locations."
)
trusted_location_ids = {
location.id
for location in entra_client.named_locations
if location.is_trusted
}
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
locations = policy.conditions.locations
if not locations:
continue
if "All" not in locations.include_locations:
continue
# A trusted-location exclusion must exist so trusted networks keep access.
if not self._excludes_only_trusted(
locations.exclude_locations, trusted_location_ids
):
continue
if (
ConditionalAccessGrantControl.BLOCK
not in policy.grant_controls.built_in_controls
):
continue
report = CheckReportM365(
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}' blocks untrusted locations but is in report-only mode."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks access from untrusted locations."
break
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_trusted_named_location_exists",
"CheckTitle": "A trusted IP-range named location is defined",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Conditional Access **named locations** let an organization define **trusted** IP ranges that can be referenced by Conditional Access policies. At least one **IP-range** named location should be marked as **Trusted** and have at least one IP range defined.",
"Risk": "Without a trusted named location, **Conditional Access** policies cannot differentiate corporate networks from untrusted networks, limiting the ability to build location-aware controls such as blocking access from untrusted locations.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-assignment-network"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Conditional Access** > **Named locations**\n3. Create a **new IP ranges location**, add the organization's public IP ranges\n4. Mark the location as **Trusted**\n5. Click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Define at least one trusted IP-range named location representing the organization's corporate networks so it can be referenced by location-aware Conditional Access policies.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_trusted_named_location_exists"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,53 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_conditional_access_trusted_named_location_exists(Check):
"""Check if at least one trusted IP-range named location is defined.
Conditional Access named locations let an organization define trusted IP ranges.
At least one IP-range named location should be marked as trusted and have at
least one IP range defined.
- PASS: A trusted IP-range named location with at least one IP range exists.
- FAIL: No trusted IP-range named location with an IP range is defined.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
named_locations = entra_client.named_locations
report = CheckReportM365(
metadata=self.metadata(),
resource=named_locations if named_locations else {},
resource_name="Named Locations",
resource_id="namedLocations",
)
report.status = "FAIL"
report.status_extended = (
"No trusted IP-range named location with at least one IP range is defined."
)
for location in named_locations:
if (
location.is_ip_location
and location.is_trusted
and location.ip_ranges_count >= 1
):
report = CheckReportM365(
metadata=self.metadata(),
resource=location,
resource_name=location.display_name or "Named Location",
resource_id=location.id,
)
report.status = "PASS"
report.status_extended = (
f"Trusted IP-range named location '{location.display_name or location.id}' "
f"is defined with {location.ip_ranges_count} IP range(s)."
)
break
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_device_registration_global_admins_not_local_admins",
"CheckTitle": "Global Administrators are not added as local administrators during Entra join",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant device registration policy should not automatically add the **Global Administrator** role to the local administrators group of a device during the Microsoft Entra join process (**azureADJoin.localAdmins.enableGlobalAdmins** should be false).",
"Risk": "Automatically granting Global Administrators local admin rights on every Entra-joined device broadens the blast radius of a device compromise and violates least privilege, since local admin rights on endpoints are rarely required for directory administration.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/devices/assign-local-admin"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Global administrator role is added as local administrator on the device during Microsoft Entra join** to **No**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable the automatic addition of Global Administrators to the local administrators group during Microsoft Entra join and grant local admin rights only where required.",
"Url": "https://hub.prowler.com/check/entra_device_registration_global_admins_not_local_admins"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,44 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_device_registration_global_admins_not_local_admins(Check):
"""Check if Global Administrators are not added as local admins on Entra join.
The device registration policy should not automatically add the Global
Administrator role to the local administrators group of a device during the
Microsoft Entra join process.
- PASS: Global Administrators are not added as local administrators on Entra join.
- FAIL: Global Administrators are added as local administrators on Entra join.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
policy = entra_client.device_registration_policy
if not policy:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name="Device Registration Policy",
resource_id="deviceRegistrationPolicy",
)
report.status = "FAIL"
report.status_extended = (
"Global Administrators are added as local administrators on devices "
"during Microsoft Entra join."
)
if policy.azure_ad_join_global_admins_enabled is False:
report.status = "PASS"
report.status_extended = (
"Global Administrators are not added as local administrators on "
"devices during Microsoft Entra join."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_device_registration_join_restricted",
"CheckTitle": "Users allowed to join devices to Microsoft Entra are restricted",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant device registration policy should restrict who can register devices as **Microsoft Entra joined** to **Selected** users or **None**. Allowing all users to join devices increases the number of devices that establish a trust relationship with the tenant.",
"Risk": "When all users can Entra-join devices, an attacker who compromises any account can register a device, potentially satisfying device-based **Conditional Access** controls and expanding their foothold in the tenant.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/devices/device-join-plan"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Users may join devices to Microsoft Entra** to **Selected** (and choose the allowed users/groups) or **None**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict Entra device join to a selected set of users or disable it entirely unless there is a business need for all users to join devices.",
"Url": "https://hub.prowler.com/check/entra_device_registration_join_restricted"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,51 @@
from typing import List
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 (
DeviceRegistrationMembershipType,
)
RESTRICTED_MEMBERSHIP_TYPES = {
DeviceRegistrationMembershipType.ENUMERATED.value,
DeviceRegistrationMembershipType.NONE.value,
}
class entra_device_registration_join_restricted(Check):
"""Check if the users allowed to join devices to Entra are restricted.
The device registration policy should restrict who can register devices as
Microsoft Entra joined to Selected users or None, rather than allowing all
users.
- PASS: Only selected users or no users may join devices to Entra.
- FAIL: All users may join devices to Entra.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
policy = entra_client.device_registration_policy
if not policy:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name="Device Registration Policy",
resource_id="deviceRegistrationPolicy",
)
report.status = "FAIL"
report.status_extended = (
"All users are allowed to join devices to Microsoft Entra."
)
if policy.azure_ad_join_allowed_to_join_type in RESTRICTED_MEMBERSHIP_TYPES:
report.status = "PASS"
report.status_extended = (
"Only selected users or no users are allowed to join devices to "
"Microsoft Entra."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_device_registration_laps_enabled",
"CheckTitle": "Microsoft Entra Local Administrator Password Solution (LAPS) is enabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant device registration policy should enable **Microsoft Entra Local Administrator Password Solution (LAPS)** (**localAdminPassword.isEnabled**). LAPS securely manages and rotates the built-in local administrator password on Windows devices and stores it for controlled retrieval.",
"Risk": "Without **LAPS**, local administrator passwords are often static and shared across devices, enabling **lateral movement**: an attacker who recovers one device's local admin password can reuse it across the fleet.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/devices/howto-manage-local-admin-passwords"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Enable Microsoft Entra Local Administrator Password Solution (LAPS)** to **Yes**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Microsoft Entra LAPS and deploy a matching Intune policy so local administrator passwords are unique per device, rotated automatically, and retrievable only by authorized roles.",
"Url": "https://hub.prowler.com/check/entra_device_registration_laps_enabled"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,42 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_device_registration_laps_enabled(Check):
"""Check if Microsoft Entra Local Administrator Password Solution (LAPS) is enabled.
The device registration policy should enable LAPS so that the built-in local
administrator password on Windows devices is securely managed and rotated.
- PASS: LAPS is enabled.
- FAIL: LAPS is disabled.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
policy = entra_client.device_registration_policy
if not policy:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name="Device Registration Policy",
resource_id="deviceRegistrationPolicy",
)
report.status = "FAIL"
report.status_extended = (
"Microsoft Entra Local Administrator Password Solution (LAPS) is disabled."
)
if policy.local_admin_password_enabled:
report.status = "PASS"
report.status_extended = (
"Microsoft Entra Local Administrator Password Solution (LAPS) is "
"enabled."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_device_registration_max_devices_per_user_limited",
"CheckTitle": "Maximum number of devices per user is limited",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant device registration policy should limit the maximum number of Microsoft Entra joined or registered devices per user to **10 or less** (**userDeviceQuota**). Once the limit is reached, no additional devices can be added until existing ones are removed.",
"Risk": "An unbounded or high per-user device quota lets a compromised account register many devices, increasing the number of trusted endpoints an attacker controls and complicating device lifecycle governance.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/devices/device-join-plan"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Maximum number of devices per user** to **10** or less\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Set the maximum number of devices per user to 10 or less to bound the number of trusted endpoints each identity can register.",
"Url": "https://hub.prowler.com/check/entra_device_registration_max_devices_per_user_limited"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,47 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
# CIS recommends a maximum of 10 devices per user (or less).
MAX_DEVICES_PER_USER = 10
class entra_device_registration_max_devices_per_user_limited(Check):
"""Check if the maximum number of devices per user is limited.
The device registration policy should set the maximum number of Entra joined or
registered devices per user to 10 or less.
- PASS: The maximum number of devices per user is 10 or less.
- FAIL: The maximum number of devices per user is greater than 10 (or unlimited).
"""
def execute(self) -> List[CheckReportM365]:
findings = []
policy = entra_client.device_registration_policy
if not policy:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name="Device Registration Policy",
resource_id="deviceRegistrationPolicy",
)
quota = policy.user_device_quota
report.status = "FAIL"
report.status_extended = (
f"The maximum number of devices per user is {quota}, which exceeds the "
f"recommended limit of {MAX_DEVICES_PER_USER}."
)
if quota is not None and quota <= MAX_DEVICES_PER_USER:
report.status = "PASS"
report.status_extended = (
f"The maximum number of devices per user is {quota}, within the "
f"recommended limit of {MAX_DEVICES_PER_USER}."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_device_registration_registering_user_not_local_admin",
"CheckTitle": "Registering user is not added as local administrator during Entra join",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant device registration policy should restrict which registering users are added to the local administrators group during Microsoft Entra join to **Selected** users or **None** (**azureADJoin.localAdmins.registeringUsers**), rather than granting local admin to every user who registers a device.",
"Risk": "Automatically granting the registering user local administrator rights gives standard users elevated control over their devices, increasing the impact of endpoint compromise and enabling local privilege abuse.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/devices/assign-local-admin"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Registering user is added as local administrator on the device during Microsoft Entra join** to **Selected** or **None**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict or disable the automatic addition of the registering user to the local administrators group during Microsoft Entra join, granting local admin rights only to selected users where required.",
"Url": "https://hub.prowler.com/check/entra_device_registration_registering_user_not_local_admin"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,53 @@
from typing import List
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 (
DeviceRegistrationMembershipType,
)
RESTRICTED_MEMBERSHIP_TYPES = {
DeviceRegistrationMembershipType.ENUMERATED.value,
DeviceRegistrationMembershipType.NONE.value,
}
class entra_device_registration_registering_user_not_local_admin(Check):
"""Check if the registering user is not added as local admin on Entra join.
The device registration policy should restrict which registering users are added
to the local administrators group during Microsoft Entra join to Selected users
or None, rather than all registering users.
- PASS: Registering users are restricted (Selected or None) from becoming local
administrators on Entra join.
- FAIL: All registering users are added as local administrators on Entra join.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
policy = entra_client.device_registration_policy
if not policy:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name="Device Registration Policy",
resource_id="deviceRegistrationPolicy",
)
report.status = "FAIL"
report.status_extended = (
"All registering users are added as local administrators on devices "
"during Microsoft Entra join."
)
if policy.azure_ad_join_registering_users_type in RESTRICTED_MEMBERSHIP_TYPES:
report.status = "PASS"
report.status_extended = (
"Registering users are restricted from being added as local "
"administrators on devices during Microsoft Entra join."
)
findings.append(report)
return findings
@@ -10,7 +10,7 @@
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "When on-premises directory synchronization is enabled, both blockSoftMatchEnabled and blockCloudObjectTakeoverThroughHardMatchEnabled must be true. Without these blocks, an attacker who can write to on-premises AD can craft an object that matches a privileged cloud account and take it over.",
"Risk": "An attacker with write access to on-premises Active Directory can create an object whose UPN, SMTP address, or ImmutableID matches an existing cloud-only account (e.g. Global Administrator). When the sync engine processes this object, it merges the on-premises identity into the cloud account, effectively granting the attacker full control of that privileged account.",
"Risk": "An attacker with write access to **on-premises** Active Directory can create an object whose UPN, SMTP address, or ImmutableID matches an existing cloud-only account (e.g. **Global Administrator**). When the sync engine processes this object, it merges the on-premises identity into the cloud account, effectively granting the attacker full control of that privileged account.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/graph/api/resources/onpremisesdirectorysynchronization?view=graph-rest-1.0",
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_idle_session_timeout_configured",
"CheckTitle": "Idle session timeout of 3 hours or less is configured",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "An activity-based timeout policy should sign out inactive users from Microsoft 365 web apps after a period of inactivity. The web session idle timeout (**WebSessionIdleTimeout**) should be set to **3 hours or less** so that unattended sessions are terminated.",
"Risk": "Without an idle session timeout, an unattended and unlocked session on a shared or public device remains authenticated indefinitely, allowing anyone with physical access to act as the signed-in user.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/microsoft-365/admin/manage/idle-session-timeout-web-apps"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft 365 admin center at https://admin.microsoft.com/\n2. Go to **Settings** > **Org settings** > **Security & Privacy** > **Idle session timeout**\n3. Turn on the setting and set the period of inactivity to **3 hours** or less\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure an idle session timeout of 3 hours or less so inactive users are automatically signed out of Microsoft 365 web apps.",
"Url": "https://hub.prowler.com/check/entra_idle_session_timeout_configured"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,57 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
# CIS recommends an idle session timeout of 3 hours or less.
MAX_IDLE_TIMEOUT_SECONDS = 3 * 60 * 60
class entra_idle_session_timeout_configured(Check):
"""Check if an idle session timeout of 3 hours or less is configured.
An activity-based timeout policy should sign out inactive users from Microsoft
365 web apps after a period of inactivity. The web session idle timeout should be
set to 3 hours or less.
- PASS: An activity-based timeout policy enforces an idle timeout of 3 hours or
less.
- FAIL: No activity-based timeout policy enforces an idle timeout of 3 hours or
less.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
policies = entra_client.activity_based_timeout_policies
report = CheckReportM365(
metadata=self.metadata(),
resource=policies if policies else {},
resource_name="Activity Based Timeout Policies",
resource_id="activityBasedTimeoutPolicies",
)
report.status = "FAIL"
report.status_extended = (
"No activity-based timeout policy enforces an idle session timeout of 3 "
"hours or less."
)
for policy in policies:
timeout = policy.web_session_idle_timeout_seconds
if timeout is not None and timeout <= MAX_IDLE_TIMEOUT_SECONDS:
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.display_name
or "Activity Based Timeout Policy",
resource_id=policy.id,
)
report.status = "PASS"
report.status_extended = (
f"Activity-based timeout policy '{policy.display_name or policy.id}' "
f"enforces an idle session timeout of {timeout // 60} minutes."
)
break
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_password_protection_custom_banned_list_enforced",
"CheckTitle": "Entra custom banned password list is enforced",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant Password Rule Settings should enforce a **custom banned password list** (**EnableBannedPasswordCheck** true with a non-empty **BannedPasswordList**). This blocks organization-specific weak or predictable passwords (e.g., company name, products, locations) in addition to Microsoft's global banned list.",
"Risk": "Without a custom **banned password** list, users can choose passwords that are predictable for the specific organization (brand names, local terms), which are easy targets for **password spraying** and guessing attacks.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Password protection**\n3. Set **Enforce custom list** to **Yes**\n4. Add organization-specific terms to the **Custom banned password list**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the custom banned password list and populate it with terms relevant to the organization to strengthen protection against weak passwords beyond the global banned list.",
"Url": "https://hub.prowler.com/check/entra_password_protection_custom_banned_list_enforced"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,51 @@
from typing import List
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 (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
class entra_password_protection_custom_banned_list_enforced(Check):
"""Check if the Entra custom banned password list is enforced.
The Password Rule Settings directory setting should enforce a custom banned
password list (EnableBannedPasswordCheck) with a non-empty BannedPasswordList so
that organization-specific weak passwords are rejected in addition to the global
banned list.
- PASS: The custom banned password list is enforced and non-empty.
- FAIL: The custom banned password list is not enforced or is empty.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
report = CheckReportM365(
metadata=self.metadata(),
resource=settings or {},
resource_name="Password Rule Settings",
resource_id=PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
report.status = "FAIL"
report.status_extended = (
"The custom banned password list is not enforced in the tenant."
)
if settings:
enforced = (
str(settings.get("EnableBannedPasswordCheck", "")).lower() == "true"
)
banned_list = settings.get("BannedPasswordList", "") or ""
if enforced and banned_list.strip():
report.status = "PASS"
report.status_extended = (
"The custom banned password list is enforced in the tenant."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_password_protection_lockout_duration_configured",
"CheckTitle": "Smart lockout duration is set to 60 seconds or more",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant Password Rule Settings should set the smart **LockoutDurationInSeconds** to **60 or more**. The lockout duration determines how long an account remains locked out before the user can attempt to sign in again.",
"Risk": "A short lockout duration allows attackers to resume **brute-force** or **password-spray** attempts sooner, reducing the effectiveness of **smart lockout** as a throttling control.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-smart-lockout"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Password protection**\n3. Set **Lockout duration in seconds** to **60** or higher\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Set the smart lockout duration to 60 seconds or more so locked-out accounts remain locked long enough to throttle automated password attacks.",
"Url": "https://hub.prowler.com/check/entra_password_protection_lockout_duration_configured"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,54 @@
from typing import List
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 (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
# CIS recommends a lockout duration of 60 seconds or more.
MIN_LOCKOUT_DURATION_SECONDS = 60
class entra_password_protection_lockout_duration_configured(Check):
"""Check if the smart lockout duration is set to 60 seconds or more.
The Password Rule Settings directory setting should set LockoutDurationInSeconds
to 60 or more so a locked-out account remains locked long enough to slow down
automated attacks.
- PASS: The lockout duration is 60 seconds or more.
- FAIL: The lockout duration is less than 60 seconds or not configured.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
report = CheckReportM365(
metadata=self.metadata(),
resource=settings or {},
resource_name="Password Rule Settings",
resource_id=PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
report.status = "FAIL"
report.status_extended = (
"The smart lockout duration is not set to 60 seconds or more."
)
if settings:
try:
duration = int(settings.get("LockoutDurationInSeconds"))
except (TypeError, ValueError):
duration = None
if duration is not None and duration >= MIN_LOCKOUT_DURATION_SECONDS:
report.status = "PASS"
report.status_extended = (
f"The smart lockout duration is set to {duration} seconds, at or "
f"above the recommended minimum of {MIN_LOCKOUT_DURATION_SECONDS}."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_password_protection_lockout_threshold_limited",
"CheckTitle": "Smart lockout threshold is set to 10 or less",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant Password Rule Settings should set the smart **LockoutThreshold** to **10 or less**. The lockout threshold determines how many failed sign-in attempts are permitted before an account is placed in a locked-out state.",
"Risk": "A high lockout threshold gives attackers more attempts per account during **password spraying** and **brute-force** attacks before lockout is triggered, increasing the chance of a successful credential compromise.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-smart-lockout"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Password protection**\n3. Set **Lockout threshold** to **10** or less\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Set the smart lockout threshold to 10 or less so accounts lock after a small number of failed sign-in attempts, limiting brute-force and password-spray attacks.",
"Url": "https://hub.prowler.com/check/entra_password_protection_lockout_threshold_limited"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,51 @@
from typing import List
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 (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
# CIS recommends a smart lockout threshold of 10 or less.
MAX_LOCKOUT_THRESHOLD = 10
class entra_password_protection_lockout_threshold_limited(Check):
"""Check if the smart lockout threshold is set to 10 or less.
The Password Rule Settings directory setting should set LockoutThreshold to 10 or
less so that accounts are locked after a small number of failed sign-in attempts.
- PASS: The lockout threshold is 10 or less.
- FAIL: The lockout threshold is greater than 10 or not configured.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
report = CheckReportM365(
metadata=self.metadata(),
resource=settings or {},
resource_name="Password Rule Settings",
resource_id=PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
report.status = "FAIL"
report.status_extended = "The smart lockout threshold is not set to 10 or less."
if settings:
try:
threshold = int(settings.get("LockoutThreshold"))
except (TypeError, ValueError):
threshold = None
if threshold is not None and threshold <= MAX_LOCKOUT_THRESHOLD:
report.status = "PASS"
report.status_extended = (
f"The smart lockout threshold is set to {threshold}, within the "
f"recommended limit of {MAX_LOCKOUT_THRESHOLD}."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_password_protection_on_premises_enforced",
"CheckTitle": "Entra password protection is enforced on on-premises Active Directory",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant Password Rule Settings should enable **password protection on Windows Server Active Directory** (**EnableBannedPasswordCheckOnPremises**) with the mode set to **Enforced**. This extends Entra banned-password checks to on-premises password changes in hybrid environments. This control only applies to tenants with on-premises directory synchronization.",
"Risk": "Without **on-premises** enforcement, users in hybrid environments can set weak or banned passwords directly in Active Directory, bypassing Entra password protection and weakening the organization's overall password posture.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad-on-premises"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Password protection**\n3. Set **Enable password protection on Windows Server Active Directory** to **Yes**\n4. Set **Mode** to **Enforced**\n5. Click **Save** (requires the Entra Password Protection agents deployed on-premises)",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable on-premises Entra password protection in Enforced mode and deploy the password protection proxy and DC agents so banned-password rules apply to on-premises password changes.",
"Url": "https://hub.prowler.com/check/entra_password_protection_on_premises_enforced"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,53 @@
from typing import List
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 (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
class entra_password_protection_on_premises_enforced(Check):
"""Check if Entra password protection is enforced on on-premises Active Directory.
The Password Rule Settings directory setting should enable password protection on
Windows Server Active Directory (EnableBannedPasswordCheckOnPremises) with the
mode set to Enforced, so banned-password rules apply to hybrid on-premises
password changes.
- PASS: On-premises password protection is enabled and set to Enforced.
- FAIL: On-premises password protection is disabled or set to Audit only.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
report = CheckReportM365(
metadata=self.metadata(),
resource=settings or {},
resource_name="Password Rule Settings",
resource_id=PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
report.status = "FAIL"
report.status_extended = (
"On-premises password protection is not enforced in the tenant."
)
if settings:
enabled = (
str(settings.get("EnableBannedPasswordCheckOnPremises", "")).lower()
== "true"
)
mode = str(settings.get("BannedPasswordCheckOnPremisesMode", "")).lower()
if enabled and mode == "enforced":
report.status = "PASS"
report.status_extended = (
"On-premises password protection is enabled and enforced in the "
"tenant."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_pim_global_administrator_approval_required",
"CheckTitle": "PIM requires approval to activate the Global Administrator role",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Privileged Identity Management (PIM) should require **approval to activate** the **Global Administrator** role, with at least one approver configured. This adds a human authorization step before the most privileged role in the tenant can be used. Requires a Microsoft Entra ID P2 license.",
"Risk": "Without approval to activate **Global Administrator**, an eligible or compromised account can self-activate the highest-privilege role without oversight, enabling immediate tenant-wide compromise.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-how-to-change-default-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **ID Governance** > **Privileged Identity Management** > **Microsoft Entra Roles** > **Roles**\n3. Select **Global Administrator** > **Role settings**\n4. Set **Require approval to activate** to **Yes** and add at least one approver\n5. Save the settings",
"Terraform": ""
},
"Recommendation": {
"Text": "Require approval to activate the Global Administrator role in PIM and configure at least one approver so activations are authorized before use.",
"Url": "https://hub.prowler.com/check/entra_pim_global_administrator_approval_required"
}
},
"Categories": [
"identity-access",
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Requires Microsoft Entra ID P2. If PIM role settings are unavailable (no P2), the check returns no findings for this role."
}
@@ -0,0 +1,48 @@
from typing import List
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 (
GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID,
)
class entra_pim_global_administrator_approval_required(Check):
"""Check if PIM requires approval to activate the Global Administrator role.
Privileged Identity Management (PIM) should require approval to activate the
Global Administrator role, with at least one approver configured.
- PASS: Approval is required to activate Global Administrator and approvers exist.
- FAIL: Approval is not required, no approvers are configured, or PIM settings are
unavailable.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
setting = entra_client.pim_role_approval_settings.get(
GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID
)
if not setting:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=setting,
resource_name="Global Administrator PIM Role Settings",
resource_id=GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID,
)
report.status = "FAIL"
report.status_extended = (
"PIM does not require approval to activate the Global Administrator role."
)
if setting.is_approval_required and setting.has_approvers:
report.status = "PASS"
report.status_extended = (
"PIM requires approval to activate the Global Administrator role and "
"has approvers configured."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_pim_privileged_role_administrator_approval_required",
"CheckTitle": "PIM requires approval to activate the Privileged Role Administrator role",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Privileged Identity Management (PIM) should require **approval to activate** the **Privileged Role Administrator** role, with at least one approver configured. This role can manage role assignments, so its activation should require human authorization. Requires a Microsoft Entra ID P2 license.",
"Risk": "Without approval to activate **Privileged Role Administrator**, an eligible or compromised account can self-activate a role that grants control over role assignments, enabling privilege escalation across the tenant.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-how-to-change-default-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **ID Governance** > **Privileged Identity Management** > **Microsoft Entra Roles** > **Roles**\n3. Select **Privileged Role Administrator** > **Role settings**\n4. Set **Require approval to activate** to **Yes** and add at least one approver\n5. Save the settings",
"Terraform": ""
},
"Recommendation": {
"Text": "Require approval to activate the Privileged Role Administrator role in PIM and configure at least one approver so activations are authorized before use.",
"Url": "https://hub.prowler.com/check/entra_pim_privileged_role_administrator_approval_required"
}
},
"Categories": [
"identity-access",
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Requires Microsoft Entra ID P2. If PIM role settings are unavailable (no P2), the check returns no findings for this role."
}
@@ -0,0 +1,50 @@
from typing import List
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 (
PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID,
)
class entra_pim_privileged_role_administrator_approval_required(Check):
"""Check if PIM requires approval to activate the Privileged Role Administrator role.
Privileged Identity Management (PIM) should require approval to activate the
Privileged Role Administrator role, with at least one approver configured.
- PASS: Approval is required to activate Privileged Role Administrator and
approvers exist.
- FAIL: Approval is not required, no approvers are configured, or PIM settings are
unavailable.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
setting = entra_client.pim_role_approval_settings.get(
PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID
)
if not setting:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=setting,
resource_name="Privileged Role Administrator PIM Role Settings",
resource_id=PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID,
)
report.status = "FAIL"
report.status_extended = (
"PIM does not require approval to activate the Privileged Role "
"Administrator role."
)
if setting.is_approval_required and setting.has_approvers:
report.status = "PASS"
report.status_extended = (
"PIM requires approval to activate the Privileged Role Administrator "
"role and has approvers configured."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_policy_default_user_cannot_create_m365_groups",
"CheckTitle": "Non-admin users cannot create Microsoft 365 groups",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The tenant Group.Unified directory setting should have **EnableGroupCreation** set to false so that non-admin users cannot create Microsoft 365 groups through the portal, API, or PowerShell. Microsoft 365 group creation should be delegated to a controlled set of users.",
"Risk": "When any user can create Microsoft 365 groups, they can provision associated resources (SharePoint sites, Teams, mailboxes) without oversight, leading to group sprawl, ungoverned data locations, and a larger attack surface.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Groups** > **General**\n3. Set **Users can create Microsoft 365 groups in Azure portals, API or PowerShell** to **No**\n4. Optionally grant creation rights to a specific security group\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable self-service Microsoft 365 group creation for non-admin users and delegate creation to an approved security group as needed.",
"Url": "https://hub.prowler.com/check/entra_policy_default_user_cannot_create_m365_groups"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
Loaded 100 of 203 files, more files were not shown because too many files have changed in this diff. Show more