feat(m365): add CIS M365 v7.0.0 entra password protection and default user permission checks (#12153)

Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
Pedro Martín
2026-08-07 13:42:52 +01:00
committed by GitHub
co-authored by Hugo P.Brito
parent 34b4e6f016
commit f2a00f19aa
33 changed files with 1352 additions and 8 deletions
@@ -0,0 +1 @@
7 M365 Entra checks covering CIS Microsoft 365 Foundations Benchmark v7.0.0 password protection, default user permissions, and guest invitation domain restrictions
+21 -7
View File
@@ -1166,7 +1166,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",
@@ -1229,7 +1231,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",
@@ -1526,7 +1530,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",
@@ -2016,7 +2022,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",
@@ -2037,7 +2045,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",
@@ -2146,7 +2156,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",
@@ -2167,7 +2179,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",
@@ -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,63 @@
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]:
"""Execute the custom banned password list enforcement check.
Evaluate whether the Password Rule Settings directory setting enforces a
non-empty custom banned password list. When the settings object is absent,
no finding is produced.
Returns:
List[CheckReportM365]: A list with a single report when the Password Rule
Settings exist, or an empty list when they are absent.
"""
findings = []
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
if not settings:
return findings
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,62 @@
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]:
"""Execute the smart lockout duration check.
Returns:
List[CheckReportM365]: Reports for the Password Rule Settings, or an
empty list when the settings are absent.
"""
findings = []
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
if not settings:
return findings
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,63 @@
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]:
"""Execute the smart lockout threshold check.
Evaluate whether the Password Rule Settings directory setting limits the smart
lockout threshold to the recommended maximum. When the settings object is
absent, no finding is produced.
Returns:
List[CheckReportM365]: A list with a single report when the Password Rule
Settings exist, or an empty list when they are absent.
"""
findings = []
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
if not settings:
return findings
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,75 @@
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.
This check applies only to hybrid tenants with on-premises synchronization.
- 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]:
"""Execute the on-premises password protection enforcement check.
Evaluate whether the Password Rule Settings directory setting enables and
enforces banned-password protection for on-premises Active Directory. When the
settings object is absent or the tenant is confirmed cloud-only, no finding is
produced.
Returns:
List[CheckReportM365]: A list with a single report when the Password Rule
Settings exist for a hybrid or unknown tenant, or an empty list when they
are absent or the tenant is confirmed cloud-only.
"""
findings = []
organizations = entra_client.organizations or []
if organizations and not any(
organization.on_premises_sync_enabled for organization in organizations
):
return findings
settings = entra_client.directory_settings.get(
PASSWORD_RULE_SETTINGS_TEMPLATE_ID
)
if not settings:
return findings
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_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": ""
}
@@ -0,0 +1,55 @@
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 (
GROUP_UNIFIED_SETTINGS_TEMPLATE_ID,
)
class entra_policy_default_user_cannot_create_m365_groups(Check):
"""Check if default users are restricted from creating Microsoft 365 groups.
The Group.Unified directory setting should have EnableGroupCreation set to false
so that non-admin users cannot create Microsoft 365 groups. If the setting does
not exist, the tenant uses the default, which allows all users to create groups.
- PASS: Non-admin users cannot create Microsoft 365 groups.
- FAIL: Non-admin users are allowed to create Microsoft 365 groups.
"""
def execute(self) -> List[CheckReportM365]:
"""Evaluate whether default users can create Microsoft 365 groups.
Inspects the Group.Unified directory setting to determine whether non-admin
users are allowed to create Microsoft 365 groups. When the setting is absent
the tenant default (group creation allowed) applies.
Returns:
List[CheckReportM365]: A single report indicating whether non-admin users
are restricted from creating Microsoft 365 groups.
"""
findings = []
settings = entra_client.directory_settings.get(
GROUP_UNIFIED_SETTINGS_TEMPLATE_ID
)
report = CheckReportM365(
metadata=self.metadata(),
resource=settings or {},
resource_name="Group.Unified Settings",
resource_id=GROUP_UNIFIED_SETTINGS_TEMPLATE_ID,
)
report.status = "FAIL"
report.status_extended = (
"Non-admin users are allowed to create Microsoft 365 groups."
)
if settings and str(settings.get("EnableGroupCreation", "")).lower() == "false":
report.status = "PASS"
report.status_extended = (
"Non-admin users are not allowed to create Microsoft 365 groups."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_policy_default_user_cannot_create_security_groups",
"CheckTitle": "Non-admin users cannot create security groups",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra tenant's authorization policy should restrict **non-admin users** from creating **security groups**. Security groups can be used to grant access to resources across Microsoft 365, so their creation should be limited to administrators to preserve least privilege and prevent uncontrolled access grants.",
"Risk": "When any user can create security groups, they may grant themselves or others access to resources, circumventing governance controls. Uncontrolled group sprawl also complicates access reviews and increases the attack surface for privilege escalation.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Groups** > **All groups** > **General**\n3. Under **Security groups**, set **Users can create security groups in Azure portals, API or PowerShell** to **No**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict security group creation to administrators by disabling self-service security group creation for non-admin users. Grant group-creation rights only to specific roles or delegated owners as required.",
"Url": "https://hub.prowler.com/check/entra_policy_default_user_cannot_create_security_groups"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,49 @@
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_policy_default_user_cannot_create_security_groups(Check):
"""Check if default users are restricted from creating security groups.
This check verifies whether the authorization policy prevents non-admin users
from creating security groups in Microsoft Entra ID.
- PASS: Non-admin users cannot create security groups.
- FAIL: Non-admin users are allowed to create security groups.
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the check for security group creation restrictions.
This method examines the authorization policy settings to determine if
non-admin users are allowed to create security groups. If security group
creation is restricted, the check passes.
Returns:
List[CheckReportM365]: A list containing the result of the check.
"""
findings = []
auth_policy = entra_client.authorization_policy
report = CheckReportM365(
metadata=self.metadata(),
resource=auth_policy if auth_policy else {},
resource_name=auth_policy.name if auth_policy else "Authorization Policy",
resource_id=auth_policy.id if auth_policy else "authorizationPolicy",
)
report.status = "FAIL"
report.status_extended = (
"Non-admin users are allowed to create security groups."
)
permissions = getattr(auth_policy, "default_user_role_permissions", None)
if permissions and permissions.allowed_to_create_security_groups is False:
report.status = "PASS"
report.status_extended = (
"Non-admin users are not allowed to create security groups."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "m365",
"CheckID": "entra_policy_guest_invitations_restricted_to_allowed_domains",
"CheckTitle": "Guest invitations are restricted to an allow-list of domains",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The external collaboration settings should **allow invitations only to specified domains** (most restrictive). An explicit allow-list limits B2B guest invitations to trusted partner organizations, while an empty allow-list blocks invitations from all external domains.",
"Risk": "Allowing invitations to any domain lets users invite guests from arbitrary or malicious organizations, increasing the risk of data exposure to untrusted external parties and expanding the tenant's collaboration attack surface.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/external-id/allow-deny-list"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Entra ID** > **External Identities** > **External collaboration settings**\n3. Under **Collaboration restrictions**, select **Allow invitations only to the specified domains (most restrictive)**\n4. Add trusted partner domains under **Target domains**, or leave the list empty to block all external invitations\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict guest invitations to an allow-list of trusted partner domains, or use an empty allow-list to block all external invitations.",
"Url": "https://hub.prowler.com/check/entra_policy_guest_invitations_restricted_to_allowed_domains"
}
},
"Categories": [
"identity-access",
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,60 @@
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_policy_guest_invitations_restricted_to_allowed_domains(Check):
"""Check if guest invitations are restricted by an allowed-domain policy.
The B2B collaboration policy should allow invitations only to a specified list of
domains (most restrictive). An empty allow-list blocks invitations from every
external domain and is compliant.
- PASS: Invitations are restricted to an allow-list, including an empty block-all
list.
- FAIL: Invitations are not restricted to an allow-list of domains.
"""
def execute(self) -> List[CheckReportM365]:
"""Evaluate whether guest invitations are restricted to allowed domains.
Inspects the B2B collaboration policy to determine whether guest invitations
are limited to an allow-list of domains. An empty allow-list blocks all external
invitations.
Returns:
List[CheckReportM365]: A single report indicating whether guest
invitations are restricted by an allowed-domain policy, or an empty list
when the policy is absent.
"""
findings = []
policy = entra_client.b2b_collaboration_policy
if not policy:
return findings
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name="B2B Collaboration Policy",
resource_id="b2bManagementPolicy",
)
report.status = "FAIL"
report.status_extended = (
"Guest invitations are not restricted to an allow-list of domains."
)
if policy.invitations_restricted_to_allowed_domains:
report.status = "PASS"
if policy.allowed_domains:
report.status_extended = (
"Guest invitations are restricted to an allow-list of "
f"{len(policy.allowed_domains)} domain(s)."
)
else:
report.status_extended = (
"Guest invitations are blocked for all external domains."
)
findings.append(report)
return findings
@@ -103,6 +103,8 @@ class Entra(M365Service):
self._get_app_registrations(),
self._get_exchange_mailbox_permission_service_principals(),
self._get_device_registration_policy(),
self._get_directory_settings(),
self._get_b2b_collaboration_policy(),
)
)
@@ -126,6 +128,8 @@ class Entra(M365Service):
self.device_registration_policy: Optional[DeviceRegistrationPolicy] = (
attributes[13]
)
self.directory_settings: Dict[str, Dict[str, str]] = attributes[14]
self.b2b_collaboration_policy: Optional[B2BCollaborationPolicy] = attributes[15]
self.user_accounts_status = {}
# Resolve directory-object identifiers referenced by Conditional Access
@@ -1243,6 +1247,98 @@ OAuthAppInfo
)
return device_registration_policy
async def _get_b2b_collaboration_policy(self):
"""Retrieve the legacy B2B collaboration (invitation domains) policy.
Fetches the legacy ``B2BManagementPolicy`` to determine whether invitations
are restricted to an allow-list of domains.
Returns:
Optional[B2BCollaborationPolicy]: The parsed policy, or None on error.
"""
logger.info("Entra - Getting B2B collaboration policy...")
b2b_policy = None
try:
url = "https://graph.microsoft.com/beta/legacy/policies"
builder = self.client.policies.with_url(url)
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)
# The legacy policy object has no string ``type`` discriminator, so
# match on the ``B2BManagementPolicy`` block inside the definition JSON.
for policy in data.get("value", []) or []:
matched = False
allowed_domains = []
invitations_restricted = False
for definition in policy.get("definition", []) or []:
try:
parsed = json.loads(definition)
except (TypeError, ValueError):
continue
b2b_block = parsed.get("B2BManagementPolicy")
if not b2b_block:
continue
matched = True
invitation_policy = (
b2b_block.get(
"InvitationsAllowedAndBlockedDomainsPolicy", {}
)
or {}
)
# Allow-list mode is active whenever the AllowedDomains key is
# present, even when empty (empty = block all external invites,
# the most restrictive and CIS-compliant state).
if "AllowedDomains" in invitation_policy:
invitations_restricted = True
allowed_domains = (
invitation_policy.get("AllowedDomains") or []
)
if matched:
b2b_policy = B2BCollaborationPolicy(
invitations_restricted_to_allowed_domains=invitations_restricted,
allowed_domains=allowed_domains,
)
break
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return b2b_policy
async def _get_directory_settings(self):
"""Retrieve tenant directory (group) settings from Microsoft Entra.
Fetches the ``/groupSettings`` collection and returns a mapping of each
setting's ``templateId`` to a dict of its ``name``/``value`` pairs. This
exposes the Group.Unified and Password Rule Settings templates used by the
group-creation and password-protection checks.
Returns:
Dict[str, Dict[str, str]]: Mapping of template ID to its name/value pairs.
"""
logger.info("Entra - Getting directory (group) settings...")
directory_settings: Dict[str, Dict[str, str]] = {}
try:
response = await self.client.group_settings.get()
for setting in getattr(response, "value", []) or []:
template_id = getattr(setting, "template_id", None)
if not template_id:
continue
values = {}
for value in getattr(setting, "values", []) or []:
name = getattr(value, "name", None)
if name is not None:
values[name] = getattr(value, "value", None)
directory_settings[template_id] = values
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return directory_settings
async def _get_service_principals(self):
"""Retrieve service principals owned by the audited tenant.
@@ -1976,6 +2072,11 @@ class AuthorizationPolicy(BaseModel):
guest_user_role_id: Optional[UUID]
# Well-known directory setting template IDs (from /groupSettings).
GROUP_UNIFIED_SETTINGS_TEMPLATE_ID = "62375ab9-6b52-47ed-826b-58e47e0e304b"
PASSWORD_RULE_SETTINGS_TEMPLATE_ID = "5cf42378-d67d-4f36-ba46-e8b86229381d"
class DeviceRegistrationMembershipType(str, Enum):
"""OData types for Entra device registration membership settings."""
@@ -1994,6 +2095,13 @@ class DeviceRegistrationPolicy(BaseModel):
local_admin_password_enabled: Optional[bool] = None
class B2BCollaborationPolicy(BaseModel):
"""Legacy B2B collaboration (invitation domains) policy."""
invitations_restricted_to_allowed_domains: bool = False
allowed_domains: List[str] = []
class Organization(BaseModel):
id: str
name: str
@@ -135,7 +135,7 @@ class Test_entra_password_hash_sync_enabled:
def test_empty_organization(self):
entra_client = mock.MagicMock()
entra_client.organization = []
entra_client.organizations = []
with (
mock.patch(
@@ -0,0 +1,66 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_password_protection_custom_banned_list_enforced.entra_password_protection_custom_banned_list_enforced"
class Test_entra_password_protection_custom_banned_list_enforced:
def _run(self, directory_settings):
entra_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_password_protection_custom_banned_list_enforced.entra_password_protection_custom_banned_list_enforced import (
entra_password_protection_custom_banned_list_enforced,
)
entra_client.directory_settings = directory_settings
return entra_password_protection_custom_banned_list_enforced().execute()
def test_template_absent(self):
result = self._run({})
assert len(result) == 0
def test_enforced_with_list(self):
result = self._run(
{
PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {
"EnableBannedPasswordCheck": "True",
"BannedPasswordList": "contoso\nproduct",
}
}
)
assert len(result) == 1
assert result[0].status == "PASS"
def test_enforced_but_empty(self):
result = self._run(
{
PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {
"EnableBannedPasswordCheck": "True",
"BannedPasswordList": "",
}
}
)
assert len(result) == 1
assert result[0].status == "FAIL"
def test_not_enforced(self):
result = self._run(
{
PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {
"EnableBannedPasswordCheck": "False",
"BannedPasswordList": "contoso",
}
}
)
assert len(result) == 1
assert result[0].status == "FAIL"
@@ -0,0 +1,41 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_password_protection_lockout_duration_configured.entra_password_protection_lockout_duration_configured"
class Test_entra_password_protection_lockout_duration_configured:
def _run(self, directory_settings):
entra_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_password_protection_lockout_duration_configured.entra_password_protection_lockout_duration_configured import (
entra_password_protection_lockout_duration_configured,
)
entra_client.directory_settings = directory_settings
return entra_password_protection_lockout_duration_configured().execute()
def test_template_absent(self):
assert len(self._run({})) == 0
def test_at_minimum(self):
result = self._run(
{PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {"LockoutDurationInSeconds": "60"}}
)
assert result[0].status == "PASS"
def test_below_minimum(self):
result = self._run(
{PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {"LockoutDurationInSeconds": "30"}}
)
assert result[0].status == "FAIL"
@@ -0,0 +1,41 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_password_protection_lockout_threshold_limited.entra_password_protection_lockout_threshold_limited"
class Test_entra_password_protection_lockout_threshold_limited:
def _run(self, directory_settings):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_password_protection_lockout_threshold_limited.entra_password_protection_lockout_threshold_limited import (
entra_password_protection_lockout_threshold_limited,
)
entra_client.directory_settings = directory_settings
return entra_password_protection_lockout_threshold_limited().execute()
def test_template_absent(self):
assert len(self._run({})) == 0
def test_within_limit(self):
result = self._run(
{PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {"LockoutThreshold": "10"}}
)
assert result[0].status == "PASS"
def test_exceeds_limit(self):
result = self._run(
{PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {"LockoutThreshold": "20"}}
)
assert result[0].status == "FAIL"
@@ -0,0 +1,100 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
PASSWORD_RULE_SETTINGS_TEMPLATE_ID,
Organization,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_password_protection_on_premises_enforced.entra_password_protection_on_premises_enforced"
class Test_entra_password_protection_on_premises_enforced:
def _run(self, directory_settings, organizations=None):
entra_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_password_protection_on_premises_enforced.entra_password_protection_on_premises_enforced import (
entra_password_protection_on_premises_enforced,
)
entra_client.directory_settings = directory_settings
entra_client.organizations = organizations or []
return entra_password_protection_on_premises_enforced().execute()
def test_no_resources(self):
result = self._run({})
assert len(result) == 0
def test_enabled_and_enforced(self):
result = self._run(
{
PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {
"EnableBannedPasswordCheckOnPremises": "True",
"BannedPasswordCheckOnPremisesMode": "Enforced",
}
},
[
Organization(
id="org-001",
name="Hybrid Org",
on_premises_sync_enabled=True,
)
],
)
assert result[0].status == "PASS"
def test_unknown_organizations_still_evaluates_settings(self):
result = self._run(
{
PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {
"EnableBannedPasswordCheckOnPremises": "True",
"BannedPasswordCheckOnPremisesMode": "Enforced",
}
},
[],
)
assert result[0].status == "PASS"
def test_audit_mode(self):
result = self._run(
{
PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {
"EnableBannedPasswordCheckOnPremises": "True",
"BannedPasswordCheckOnPremisesMode": "Audit",
}
},
[
Organization(
id="org-001",
name="Hybrid Org",
on_premises_sync_enabled=True,
)
],
)
assert result[0].status == "FAIL"
def test_cloud_only_tenant_has_no_finding(self):
result = self._run(
{
PASSWORD_RULE_SETTINGS_TEMPLATE_ID: {
"EnableBannedPasswordCheckOnPremises": "False",
"BannedPasswordCheckOnPremisesMode": "Audit",
}
},
[
Organization(
id="org-001",
name="Cloud Only Org",
on_premises_sync_enabled=False,
)
],
)
assert result == []
@@ -0,0 +1,45 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
GROUP_UNIFIED_SETTINGS_TEMPLATE_ID,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_m365_groups.entra_policy_default_user_cannot_create_m365_groups"
class Test_entra_policy_default_user_cannot_create_m365_groups:
def _run(self, directory_settings):
entra_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_m365_groups.entra_policy_default_user_cannot_create_m365_groups import (
entra_policy_default_user_cannot_create_m365_groups,
)
entra_client.directory_settings = directory_settings
return entra_policy_default_user_cannot_create_m365_groups().execute()
def test_template_absent(self):
result = self._run({})
assert len(result) == 1
assert result[0].status == "FAIL"
def test_group_creation_enabled(self):
result = self._run(
{GROUP_UNIFIED_SETTINGS_TEMPLATE_ID: {"EnableGroupCreation": "true"}}
)
assert len(result) == 1
assert result[0].status == "FAIL"
def test_group_creation_disabled(self):
result = self._run(
{GROUP_UNIFIED_SETTINGS_TEMPLATE_ID: {"EnableGroupCreation": "false"}}
)
assert len(result) == 1
assert result[0].status == "PASS"
@@ -0,0 +1,139 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
AuthorizationPolicy,
DefaultUserRolePermissions,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
class Test_entra_policy_default_user_cannot_create_security_groups:
def test_users_can_create_security_groups(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups import (
entra_policy_default_user_cannot_create_security_groups,
)
entra_client.authorization_policy = AuthorizationPolicy(
id="authorizationPolicy",
name="Authorization Policy",
description="",
default_user_role_permissions=DefaultUserRolePermissions(
allowed_to_create_security_groups=True,
),
)
check = entra_policy_default_user_cannot_create_security_groups()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Non-admin users are allowed to create security groups."
)
assert result[0].resource_id == "authorizationPolicy"
assert result[0].resource_name == "Authorization Policy"
def test_authorization_policy_none(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups import (
entra_policy_default_user_cannot_create_security_groups,
)
entra_client.authorization_policy = None
result = entra_policy_default_user_cannot_create_security_groups().execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_id == "authorizationPolicy"
def test_users_cannot_create_security_groups(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups import (
entra_policy_default_user_cannot_create_security_groups,
)
entra_client.authorization_policy = AuthorizationPolicy(
id="authorizationPolicy",
name="Authorization Policy",
description="",
default_user_role_permissions=DefaultUserRolePermissions(
allowed_to_create_security_groups=False,
),
)
check = entra_policy_default_user_cannot_create_security_groups()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Non-admin users are not allowed to create security groups."
)
def test_unknown_security_group_creation_permission_fails(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_create_security_groups.entra_policy_default_user_cannot_create_security_groups import (
entra_policy_default_user_cannot_create_security_groups,
)
entra_client.authorization_policy = AuthorizationPolicy(
id="authorizationPolicy",
name="Authorization Policy",
description="",
default_user_role_permissions=DefaultUserRolePermissions(
allowed_to_create_security_groups=None,
),
)
result = entra_policy_default_user_cannot_create_security_groups().execute()
assert len(result) == 1
assert result[0].status == "FAIL"
@@ -0,0 +1,64 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import B2BCollaborationPolicy
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_policy_guest_invitations_restricted_to_allowed_domains.entra_policy_guest_invitations_restricted_to_allowed_domains"
class Test_entra_policy_guest_invitations_restricted_to_allowed_domains:
def _run(self, policy):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_policy_guest_invitations_restricted_to_allowed_domains.entra_policy_guest_invitations_restricted_to_allowed_domains import (
entra_policy_guest_invitations_restricted_to_allowed_domains,
)
entra_client.b2b_collaboration_policy = policy
return (
entra_policy_guest_invitations_restricted_to_allowed_domains().execute()
)
def test_no_policy(self):
assert self._run(None) == []
def test_restricted(self):
result = self._run(
B2BCollaborationPolicy(
invitations_restricted_to_allowed_domains=True,
allowed_domains=["partner.com"],
)
)
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Guest invitations are restricted to an allow-list of 1 domain(s)."
)
def test_not_restricted(self):
result = self._run(
B2BCollaborationPolicy(
invitations_restricted_to_allowed_domains=False,
allowed_domains=[],
)
)
assert result[0].status == "FAIL"
def test_restricted_with_empty_allowed_domains(self):
result = self._run(
B2BCollaborationPolicy(
invitations_restricted_to_allowed_domains=True,
allowed_domains=[],
)
)
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Guest invitations are blocked for all external domains."
)
@@ -1,5 +1,6 @@
import asyncio
import importlib
import json
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
@@ -199,6 +200,43 @@ async def mock_entra_get_default_app_management_policy(_):
class Test_Entra_Service:
@staticmethod
def _load_b2b_policy(invitation_policy):
service = object.__new__(Entra)
service.client = MagicMock()
service.client.request_adapter.send_primitive_async = AsyncMock(
return_value=json.dumps(
{
"value": [
{
"definition": [
json.dumps(
{
"B2BManagementPolicy": {
"InvitationsAllowedAndBlockedDomainsPolicy": invitation_policy
}
}
)
]
}
]
}
).encode()
)
return asyncio.run(service._get_b2b_collaboration_policy())
def test_get_b2b_policy_empty_allowed_domains_is_restricted(self):
policy = self._load_b2b_policy({"AllowedDomains": []})
assert policy.invitations_restricted_to_allowed_domains is True
assert policy.allowed_domains == []
def test_get_b2b_policy_without_allowed_domains_is_unrestricted(self):
policy = self._load_b2b_policy({})
assert policy.invitations_restricted_to_allowed_domains is False
assert policy.allowed_domains == []
def test_get_client(self):
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
admincenter_client = Entra(