feat(m365): add CIS M365 v7.0.0 teams external access check (#12151)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Pedro MartínandDaniel Barranquero authored and GitHub committed 2026-08-05 12:24:44 +02:00
1 parent d3524d50fb
commit f87522e423
9 files changed
+182 -3

No files matched your search

@@ -0,0 +1 @@
`teams_external_access_trial_tenants_blocked` check for M365 provider, verifying that Teams external access with trial-only tenants is blocked, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 control 8.2.4
+3 -1
View File
@@ -3098,7 +3098,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",
@@ -325,7 +325,9 @@ class M365PowerShell(PowerShellSession):
"""
Get Teams User Settings.
Retrieves the current Microsoft Teams user settings.
Retrieves the current Microsoft Teams user settings. Enum-typed properties
(e.g. ExternalAccessWithTrialTenants) are serialized as their string names
rather than numeric values.
Returns:
dict: Teams user settings in JSON format.
@@ -337,7 +339,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTenantFederationConfiguration | ConvertTo-Json -Depth 10",
"Get-CsTenantFederationConfiguration | ConvertTo-Json -Depth 10 -EnumsAsStrings",
json_parse=True,
)
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "teams_external_access_trial_tenants_blocked",
"CheckTitle": "External access with Teams trial-only tenants is blocked",
"CheckType": [],
"ServiceName": "teams",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "The Teams external access configuration should **block** communication with **trial-only** tenants (tenants that don't have any purchased seats). When blocked, users from these trial-only tenants cannot search for, contact, call, or meet with the organization's users via Teams external access.",
"Risk": "Trial-only tenants can be created quickly and anonymously, making them a common vector for social engineering and **phishing**. Allowing external access with them lets untrusted actors reach employees through Teams chats, calls, and meetings.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/microsoftteams/manage-external-access"
],
"Remediation": {
"Code": {
"CLI": "Set-CsTenantFederationConfiguration -ExternalAccessWithTrialTenants Blocked",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Teams admin center at https://admin.teams.microsoft.com/\n2. Go to **External collaboration** > **External access** > **Organization settings**\n3. Set **People in my organization can communicate with accounts in trial Teams tenants** to **Off**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Block external access with Teams trial-only tenants so that users from tenants without purchased seats cannot contact your organization through Teams.",
"Url": "https://hub.prowler.com/check/teams_external_access_trial_tenants_blocked"
}
},
"Categories": [
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "The check evaluates the ExternalAccessWithTrialTenants property from Get-CsTenantFederationConfiguration. Microsoft's service default for this setting is Blocked, so if the property is absent from the cmdlet output the check assumes the default (secure) state and passes."
}
@@ -0,0 +1,46 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.teams.teams_client import teams_client
class teams_external_access_trial_tenants_blocked(Check):
"""Check if external access with Teams trial-only tenants is blocked.
This setting controls external access with Teams "trial-only" tenants (tenants
that don't have any purchased seats). When blocked, users from those tenants
cannot search for, chat, call, or meet with the organization's users.
- PASS: External access with trial-only tenants is blocked.
- FAIL: External access with trial-only tenants is allowed.
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the check for external access with trial-only Teams tenants.
Returns:
List[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
user_settings = teams_client.user_settings
if user_settings:
report = CheckReportM365(
metadata=self.metadata(),
resource=user_settings,
resource_name="Teams User Settings",
resource_id="userSettings",
)
report.status = "FAIL"
report.status_extended = (
"External access with Teams trial-only tenants is allowed."
)
if user_settings.external_access_with_trial_tenants == "Blocked":
report.status = "PASS"
report.status_extended = (
"External access with Teams trial-only tenants is blocked."
)
findings.append(report)
return findings
@@ -119,6 +119,9 @@ class Teams(M365Service):
allow_teams_consumer_inbound=settings.get(
"AllowTeamsConsumerInbound", True
),
external_access_with_trial_tenants=settings.get(
"ExternalAccessWithTrialTenants", "Blocked"
),
)
except Exception as error:
logger.error(
@@ -160,3 +163,6 @@ class UserSettings(BaseModel):
allow_external_access: bool = True
allow_teams_consumer: bool = True
allow_teams_consumer_inbound: bool = True
# Microsoft's service default for ExternalAccessWithTrialTenants is Blocked,
# so an absent property is assumed to be in the default (secure) state.
external_access_with_trial_tenants: str = "Blocked"
@@ -0,0 +1,60 @@
from unittest import mock
from prowler.providers.m365.services.teams.teams_service import UserSettings
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.teams.teams_external_access_trial_tenants_blocked.teams_external_access_trial_tenants_blocked"
class Test_teams_external_access_trial_tenants_blocked:
def _run(self, user_settings):
teams_client = mock.MagicMock()
teams_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_microsoft_teams"
),
mock.patch(f"{CHECK_MODULE_PATH}.teams_client", new=teams_client),
):
from prowler.providers.m365.services.teams.teams_external_access_trial_tenants_blocked.teams_external_access_trial_tenants_blocked import (
teams_external_access_trial_tenants_blocked,
)
teams_client.user_settings = user_settings
return teams_external_access_trial_tenants_blocked().execute()
def test_no_user_settings(self):
assert self._run(None) == []
def test_trial_tenants_blocked(self):
result = self._run(UserSettings(external_access_with_trial_tenants="Blocked"))
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "External access with Teams trial-only tenants is blocked."
)
assert result[0].resource_name == "Teams User Settings"
assert result[0].resource_id == "userSettings"
def test_trial_tenants_allowed(self):
result = self._run(UserSettings(external_access_with_trial_tenants="Allowed"))
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "External access with Teams trial-only tenants is allowed."
)
assert result[0].resource_name == "Teams User Settings"
assert result[0].resource_id == "userSettings"
def test_trial_tenants_default(self):
# ExternalAccessWithTrialTenants absent from the cmdlet output: the model
# falls back to Microsoft's service default (Blocked), so the check passes.
result = self._run(UserSettings())
assert len(result) == 1
assert result[0].status == "PASS"
@@ -48,6 +48,7 @@ def mock_get_user_settings(_):
allow_external_access=False,
allow_teams_consumer=False,
allow_teams_consumer_inbound=False,
external_access_with_trial_tenants="Blocked",
)
@@ -113,9 +114,33 @@ class Test_Teams_Service:
allow_external_access=False,
allow_teams_consumer=False,
allow_teams_consumer_inbound=False,
external_access_with_trial_tenants="Blocked",
)
teams_client.powershell.close()
def test_get_user_settings_parses_external_access_with_trial_tenants(self):
service = Teams.__new__(Teams)
service.powershell = mock.MagicMock()
service.powershell.get_user_settings.return_value = {
"AllowFederatedUsers": True,
"AllowTeamsConsumer": True,
"AllowTeamsConsumerInbound": True,
"ExternalAccessWithTrialTenants": "Allowed",
}
user_settings = Teams._get_user_settings(service)
assert user_settings.external_access_with_trial_tenants == "Allowed"
def test_get_user_settings_trial_tenants_defaults_to_blocked_when_absent(self):
# Older MicrosoftTeams module versions do not return the property; the
# parser assumes Microsoft's service default (Blocked).
service = Teams.__new__(Teams)
service.powershell = mock.MagicMock()
service.powershell.get_user_settings.return_value = {
"AllowFederatedUsers": True,
}
user_settings = Teams._get_user_settings(service)
assert user_settings.external_access_with_trial_tenants == "Blocked"
@patch(
"prowler.providers.m365.services.teams.teams_service.Teams._get_global_meeting_policy",
new=mock_get_global_meeting_policy,