diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 4537150310..461ebe7a32 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add check for Bypass Disable in every Mailbox for service Defender in M365 [(#7418)](https://github.com/prowler-cloud/prowler/pull/7418) - Add new check `teams_external_domains_restricted` [(#7557)](https://github.com/prowler-cloud/prowler/pull/7557) - Add new check `teams_email_sending_to_channel_disabled` [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533) +- Add new check `teams_meeting_anonymous_user_join_disabled` [(#7565)](https://github.com/prowler-cloud/prowler/pull/7565) - Add new check `teams_unmanaged_communication_disabled` [(#7561)](https://github.com/prowler-cloud/prowler/pull/7561) - Add new check `teams_external_users_cannot_start_conversations` [(#7562)](https://github.com/prowler-cloud/prowler/pull/7562) - Add new check for AllowList not used in the Connection Filter Policy from service Defender in M365 [(#7492)](https://github.com/prowler-cloud/prowler/pull/7492) diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 3785bac7f5..7b272cc748 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -137,6 +137,25 @@ class M365PowerShell(PowerShellSession): """ return self.execute("Get-CsTeamsClientConfiguration | ConvertTo-Json") + def get_global_meeting_policy(self) -> dict: + """ + Get Teams Global Meeting Policy. + + Retrieves the current Microsoft Teams global meeting policy settings. + + Returns: + dict: Teams global meeting policy settings in JSON format. + + Example: + >>> get_global_meeting_policy() + { + "AllowAnonymousUsersToJoinMeeting": true + } + """ + return self.execute( + "Get-CsTeamsMeetingPolicy -Identity Global | ConvertTo-Json" + ) + def get_user_settings(self) -> dict: """ Get Teams User Settings. diff --git a/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/__init__.py b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.metadata.json new file mode 100644 index 0000000000..2cadb55db7 --- /dev/null +++ b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "teams_meeting_anonymous_user_join_disabled", + "CheckTitle": "Ensure anonymous users are not able to join meetings.", + "CheckType": [], + "ServiceName": "teams", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "Teams Global Meeting Policy", + "Description": "Ensure individuals who are not sent or forwarded a meeting invite will not be able to join the meeting automatically.", + "Risk": "Allowing anonymous users to join meetings can lead to unauthorized access, information leakage, and potential disruptions, especially in meetings involving sensitive data.", + "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Remediation": { + "Code": { + "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToJoinMeeting $false", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under meeting join & lobby set Anonymous users can join a meeting to Off.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable anonymous user access to Microsoft Teams meetings to ensure only invited participants can join. This adds a layer of vetting by requiring organizer approval for anyone not explicitly invited.", + "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.py b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.py new file mode 100644 index 0000000000..c3f8df2f64 --- /dev/null +++ b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.py @@ -0,0 +1,40 @@ +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_meeting_anonymous_user_join_disabled(Check): + """Check if anonymous users are not able to join meetings. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the check for anonymous users are not able to join meetings. + + This method checks if anonymous users are not able to join meetings. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + global_meeting_policy = teams_client.global_meeting_policy + if global_meeting_policy: + report = CheckReportM365( + metadata=self.metadata(), + resource=global_meeting_policy if global_meeting_policy else {}, + resource_name="Teams Meetings Global (Org-wide default) Policy", + resource_id="teamsMeetingsGlobalPolicy", + ) + report.status = "FAIL" + report.status_extended = "Anonymous Teams users can join meetings." + + if not global_meeting_policy.allow_anonymous_users_to_join_meeting: + report.status = "PASS" + report.status_extended = "Anonymous Teams users can not join meetings." + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/teams/teams_service.py b/prowler/providers/m365/services/teams/teams_service.py index 9d1810e437..81e5797e64 100644 --- a/prowler/providers/m365/services/teams/teams_service.py +++ b/prowler/providers/m365/services/teams/teams_service.py @@ -10,6 +10,7 @@ class Teams(M365Service): super().__init__(provider) self.powershell.connect_microsoft_teams() self.teams_settings = self._get_teams_client_configuration() + self.global_meeting_policy = self._get_global_meeting_policy() self.user_settings = self._get_user_settings() self.powershell.close() @@ -38,6 +39,23 @@ class Teams(M365Service): ) return teams_settings + def _get_global_meeting_policy(self): + logger.info("M365 - Getting Teams global (org-wide default) meeting policy...") + global_meeting_policy = None + try: + global_meeting_policy = self.powershell.get_global_meeting_policy() + if global_meeting_policy: + global_meeting_policy = GlobalMeetingPolicy( + allow_anonymous_users_to_join_meeting=global_meeting_policy.get( + "AllowAnonymousUsersToJoinMeeting", True + ) + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return global_meeting_policy + def _get_user_settings(self): logger.info("M365 - Getting Teams user settings...") user_settings = None @@ -71,6 +89,10 @@ class TeamsSettings(BaseModel): allow_email_into_channel: bool = True +class GlobalMeetingPolicy(BaseModel): + allow_anonymous_users_to_join_meeting: bool = True + + class UserSettings(BaseModel): allow_external_access: bool = True allow_teams_consumer: bool = True diff --git a/tests/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled_test.py b/tests/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled_test.py new file mode 100644 index 0000000000..899f7514aa --- /dev/null +++ b/tests/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled_test.py @@ -0,0 +1,117 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_teams_meeting_anonymous_user_join_disabled: + def test_no_global_meeting_policy(self): + teams_client = mock.MagicMock() + teams_client.global_meeting_policy = None + + 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( + "prowler.providers.m365.services.teams.teams_meeting_anonymous_user_join_disabled.teams_meeting_anonymous_user_join_disabled.teams_client", + new=teams_client, + ), + ): + from prowler.providers.m365.services.teams.teams_meeting_anonymous_user_join_disabled.teams_meeting_anonymous_user_join_disabled import ( + teams_meeting_anonymous_user_join_disabled, + ) + + check = teams_meeting_anonymous_user_join_disabled() + result = check.execute() + assert len(result) == 0 + + def test_anonymous_users_can_join_meetings(self): + teams_client = mock.MagicMock() + teams_client.audited_tenant = "audited_tenant" + 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( + "prowler.providers.m365.services.teams.teams_meeting_anonymous_user_join_disabled.teams_meeting_anonymous_user_join_disabled.teams_client", + new=teams_client, + ), + ): + from prowler.providers.m365.services.teams.teams_meeting_anonymous_user_join_disabled.teams_meeting_anonymous_user_join_disabled import ( + teams_meeting_anonymous_user_join_disabled, + ) + from prowler.providers.m365.services.teams.teams_service import ( + GlobalMeetingPolicy, + ) + + teams_client.global_meeting_policy = GlobalMeetingPolicy( + allow_anonymous_users_to_join_meeting=True + ) + + check = teams_meeting_anonymous_user_join_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended == "Anonymous Teams users can join meetings." + ) + assert result[0].resource == teams_client.global_meeting_policy.dict() + assert ( + result[0].resource_name + == "Teams Meetings Global (Org-wide default) Policy" + ) + assert result[0].resource_id == "teamsMeetingsGlobalPolicy" + + def test_anonymous_users_cannot_join_meetings(self): + teams_client = mock.MagicMock() + teams_client.audited_tenant = "audited_tenant" + 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( + "prowler.providers.m365.services.teams.teams_meeting_anonymous_user_join_disabled.teams_meeting_anonymous_user_join_disabled.teams_client", + new=teams_client, + ), + ): + from prowler.providers.m365.services.teams.teams_meeting_anonymous_user_join_disabled.teams_meeting_anonymous_user_join_disabled import ( + teams_meeting_anonymous_user_join_disabled, + ) + from prowler.providers.m365.services.teams.teams_service import ( + GlobalMeetingPolicy, + ) + + teams_client.global_meeting_policy = GlobalMeetingPolicy( + allow_anonymous_users_to_join_meeting=False + ) + + check = teams_meeting_anonymous_user_join_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Anonymous Teams users can not join meetings." + ) + assert result[0].resource == teams_client.global_meeting_policy.dict() + assert ( + result[0].resource_name + == "Teams Meetings Global (Org-wide default) Policy" + ) + assert result[0].resource_id == "teamsMeetingsGlobalPolicy" diff --git a/tests/providers/m365/services/teams/teams_service_test.py b/tests/providers/m365/services/teams/teams_service_test.py index 41874155d5..a35f7f1a7b 100644 --- a/tests/providers/m365/services/teams/teams_service_test.py +++ b/tests/providers/m365/services/teams/teams_service_test.py @@ -4,6 +4,7 @@ from unittest.mock import patch from prowler.providers.m365.models import M365IdentityInfo from prowler.providers.m365.services.teams.teams_service import ( CloudStorageSettings, + GlobalMeetingPolicy, Teams, TeamsSettings, UserSettings, @@ -23,6 +24,10 @@ def mock_get_teams_client_configuration(_): ) +def mock_get_global_meeting_policy(_): + return GlobalMeetingPolicy(allow_anonymous_users_to_join_meeting=False) + + def mock_get_user_settings(_): return UserSettings( allow_external_access=False, @@ -95,3 +100,23 @@ class Test_Teams_Service: allow_teams_consumer_inbound=False, ) teams_client.powershell.close() + + @patch( + "prowler.providers.m365.services.teams.teams_service.Teams._get_global_meeting_policy", + new=mock_get_global_meeting_policy, + ) + def test_get_global_meeting_policy(self): + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_microsoft_teams" + ), + ): + teams_client = Teams( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert teams_client.global_meeting_policy == GlobalMeetingPolicy( + allow_anonymous_users_to_join_meeting=False + ) + teams_client.powershell.close()