diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 27e03c39ce..3fef12fc90 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Added +- `defender_zap_for_teams_enabled` check for M365 provider [(#9838)](https://github.com/prowler-cloud/prowler/pull/9838) - `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747) - `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840) - `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828) diff --git a/prowler/compliance/m365/cis_4.0_m365.json b/prowler/compliance/m365/cis_4.0_m365.json index 98404ca88f..156c17e26d 100644 --- a/prowler/compliance/m365/cis_4.0_m365.json +++ b/prowler/compliance/m365/cis_4.0_m365.json @@ -693,7 +693,9 @@ { "Id": "2.4.4", "Description": "Zero-hour auto purge (ZAP) is a protection feature that retroactively detects and neutralizes malware and high confidence phishing. When ZAP for Teams protection blocks a message, the message is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP occurs up to 48 hours after delivery.", - "Checks": [], + "Checks": [ + "defender_zap_for_teams_enabled" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json index a8f2a1e95d..93fe2e2226 100644 --- a/prowler/compliance/m365/cis_6.0_m365.json +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -755,7 +755,9 @@ { "Id": "2.4.4", "Description": "Zero-hour auto purge (ZAP) is a protection feature that retroactively detects and neutralizes malware and high confidence phishing. When ZAP for Teams protection blocks a message, the message is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP occurs up to 48 hours after delivery.", - "Checks": [], + "Checks": [ + "defender_zap_for_teams_enabled" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index 5e5d39c241..81ae9dea2a 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -112,12 +112,13 @@ } ], "Checks": [ - "entra_identity_protection_sign_in_risk_enabled", - "entra_identity_protection_user_risk_enabled", + "defender_antiphishing_policy_configured", "defender_antispam_outbound_policy_configured", "defender_malware_policy_notifications_internal_users_malware_enabled", - "defender_antiphishing_policy_configured", - "entra_admin_users_phishing_resistant_mfa_enabled" + "defender_zap_for_teams_enabled", + "entra_admin_users_phishing_resistant_mfa_enabled", + "entra_identity_protection_sign_in_risk_enabled", + "entra_identity_protection_user_risk_enabled" ] }, { @@ -344,15 +345,15 @@ } ], "Checks": [ - "defender_antispam_outbound_policy_configured", - "defender_malware_policy_notifications_internal_users_malware_enabled", - "defender_malware_policy_common_attachments_filter_enabled", - "defender_malware_policy_comprehensive_attachments_filter_applied", "defender_antispam_connection_filter_policy_empty_ip_allowlist", "defender_antispam_connection_filter_policy_safe_list_off", "defender_antispam_outbound_policy_configured", "defender_antispam_outbound_policy_forwarding_disabled", - "defender_antispam_policy_inbound_no_allowed_domains" + "defender_antispam_policy_inbound_no_allowed_domains", + "defender_malware_policy_common_attachments_filter_enabled", + "defender_malware_policy_comprehensive_attachments_filter_applied", + "defender_malware_policy_notifications_internal_users_malware_enabled", + "defender_zap_for_teams_enabled" ] }, { @@ -368,11 +369,11 @@ } ], "Checks": [ + "defender_antispam_outbound_policy_configured", "defender_malware_policy_common_attachments_filter_enabled", "defender_malware_policy_comprehensive_attachments_filter_applied", "defender_malware_policy_notifications_internal_users_malware_enabled", - "defender_antispam_outbound_policy_configured", - "defender_malware_policy_notifications_internal_users_malware_enabled" + "defender_zap_for_teams_enabled" ] }, { @@ -691,6 +692,7 @@ "defender_malware_policy_common_attachments_filter_enabled", "defender_malware_policy_comprehensive_attachments_filter_applied", "defender_malware_policy_notifications_internal_users_malware_enabled", + "defender_zap_for_teams_enabled", "teams_external_domains_restricted", "teams_external_users_cannot_start_conversations" ] diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 512f9f03e2..81202b5a84 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -823,6 +823,26 @@ class M365PowerShell(PowerShellSession): "Get-SharingPolicy | ConvertTo-Json -Depth 10", json_parse=True ) + def get_teams_protection_policy(self) -> dict: + """ + Get Teams Protection Policy. + + Retrieves the Teams protection policy settings including Zero-hour auto purge (ZAP) configuration. + + Returns: + dict: Teams protection policy settings in JSON format. + + Example: + >>> get_teams_protection_policy() + { + "Identity": "Teams Protection Policy", + "ZapEnabled": True + } + """ + return self.execute( + "Get-TeamsProtectionPolicy | ConvertTo-Json -Depth 10", json_parse=True + ) + def get_shared_mailboxes(self) -> dict: """ Get Exchange Online Shared Mailboxes. diff --git a/prowler/providers/m365/services/defender/defender_service.py b/prowler/providers/m365/services/defender/defender_service.py index d3ede5734a..09aa39986d 100644 --- a/prowler/providers/m365/services/defender/defender_service.py +++ b/prowler/providers/m365/services/defender/defender_service.py @@ -8,7 +8,20 @@ from prowler.providers.m365.m365_provider import M365Provider class Defender(M365Service): + """ + Microsoft 365 Defender service implementation. + + Provides access to Microsoft Defender for Office 365 configurations including + malware policies, spam filtering, anti-phishing, and Teams protection settings. + """ + def __init__(self, provider: M365Provider): + """ + Initialize the Defender service. + + Args: + provider: The M365 provider instance. + """ super().__init__(provider) self.malware_policies = [] self.outbound_spam_policies = {} @@ -20,6 +33,7 @@ class Defender(M365Service): self.inbound_spam_policies = [] self.inbound_spam_rules = {} self.report_submission_policy = None + self.teams_protection_policy = None if self.powershell: if self.powershell.connect_exchange_online(): self.malware_policies = self._get_malware_filter_policy() @@ -33,6 +47,7 @@ class Defender(M365Service): self.inbound_spam_policies = self._get_inbound_spam_filter_policy() self.inbound_spam_rules = self._get_inbound_spam_filter_rule() self.report_submission_policy = self._get_report_submission_policy() + self.teams_protection_policy = self._get_teams_protection_policy() self.powershell.close() def _get_malware_filter_policy(self): @@ -350,6 +365,12 @@ class Defender(M365Service): return inbound_spam_rules def _get_report_submission_policy(self): + """ + Retrieve the Defender report submission policy. + + Returns: + ReportSubmissionPolicy: The report submission policy configuration. + """ logger.info("Microsoft365 - Getting Defender report submission policy...") report_submission_policy = None try: @@ -387,6 +408,28 @@ class Defender(M365Service): ) return report_submission_policy + def _get_teams_protection_policy(self): + """ + Retrieve the Teams protection policy including ZAP settings. + + Returns: + TeamsProtectionPolicy: The Teams protection policy configuration. + """ + logger.info("Microsoft365 - Getting Teams protection policy...") + teams_protection_policy = None + try: + policy = self.powershell.get_teams_protection_policy() + if policy: + teams_protection_policy = TeamsProtectionPolicy( + identity=policy.get("Identity", ""), + zap_enabled=policy.get("ZapEnabled", True), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return teams_protection_policy + class MalwarePolicy(BaseModel): enable_file_filter: bool @@ -470,6 +513,8 @@ class InboundSpamRule(BaseModel): class ReportSubmissionPolicy(BaseModel): + """Model for Defender report submission policy settings.""" + report_junk_to_customized_address: bool report_not_junk_to_customized_address: bool report_phish_to_customized_address: bool @@ -478,3 +523,10 @@ class ReportSubmissionPolicy(BaseModel): report_phish_addresses: list[str] report_chat_message_enabled: bool report_chat_message_to_customized_address_enabled: bool + + +class TeamsProtectionPolicy(BaseModel): + """Model for Teams protection policy settings including ZAP configuration.""" + + identity: str + zap_enabled: bool diff --git a/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/__init__.py b/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.metadata.json new file mode 100644 index 0000000000..d2df3f8892 --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "m365", + "CheckID": "defender_zap_for_teams_enabled", + "CheckTitle": "Zero-hour auto purge (ZAP) protects Microsoft Teams from malware and phishing", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Teams Protection Policy", + "ResourceGroup": "collaboration", + "Description": "Zero-hour auto purge (ZAP) is a protection feature that retroactively detects and neutralizes **malware** and **high confidence phishing** in Teams messages.\n\nWhen ZAP blocks a message, it is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP can occur up to 48 hours after delivery.", + "Risk": "Without ZAP enabled, malicious content delivered to Teams chats remains accessible to users for up to 48 hours after delivery, even after being identified as harmful.\n\nThis extended exposure window could lead to:\n- **Malware infections** from weaponized attachments or links\n- **Phishing attacks** compromising user credentials and MFA tokens\n- **Lateral movement** as attackers exploit compromised accounts within the organization", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/zero-hour-auto-purge?view=o365-worldwide#zero-hour-auto-purge-zap-in-microsoft-teams", + "https://learn.microsoft.com/en-us/defender-office-365/mdo-support-teams-about?view=o365-worldwide" + ], + "Remediation": { + "Code": { + "CLI": "Set-TeamsProtectionPolicy -Identity 'Teams Protection Policy' -ZapEnabled $true", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft Defender https://security.microsoft.com/\n2. Click to expand System and select Settings > Email & collaboration > Microsoft Teams protection\n3. Set Zero-hour auto purge (ZAP) to On (Default)", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable Zero-hour auto purge (ZAP) for Microsoft Teams to ensure malicious content is automatically removed from chats after detection, even if it was delivered before being identified as harmful.", + "Url": "https://hub.prowler.com/check/defender_zap_for_teams_enabled" + } + }, + "Categories": [ + "email-security", + "e5" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.py b/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.py new file mode 100644 index 0000000000..6287426dbf --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.py @@ -0,0 +1,53 @@ +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_zap_for_teams_enabled(Check): + """Check if Zero-hour auto purge (ZAP) is enabled for Microsoft Teams. + + ZAP is a protection feature that retroactively detects and neutralizes malware + and high confidence phishing in Teams messages. + + - PASS: ZAP is enabled for Teams protection. + - FAIL: ZAP is not enabled for Teams protection. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the check for Teams ZAP protection status. + + This method checks if Zero-hour auto purge (ZAP) is enabled for Microsoft Teams + to ensure malicious content is automatically removed from chats after detection. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + teams_protection_policy = defender_client.teams_protection_policy + + if teams_protection_policy: + report = CheckReportM365( + metadata=self.metadata(), + resource=teams_protection_policy, + resource_name="Teams Protection Policy", + resource_id="teamsProtectionPolicy", + ) + + if teams_protection_policy.zap_enabled: + report.status = "PASS" + report.status_extended = ( + "Zero-hour auto purge (ZAP) is enabled for Microsoft Teams." + ) + else: + report.status = "FAIL" + report.status_extended = ( + "Zero-hour auto purge (ZAP) is not enabled for Microsoft Teams." + ) + + findings.append(report) + + return findings diff --git a/tests/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled_test.py b/tests/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled_test.py new file mode 100644 index 0000000000..89879ca235 --- /dev/null +++ b/tests/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled_test.py @@ -0,0 +1,119 @@ +from unittest import mock + +from prowler.providers.m365.services.defender.defender_service import ( + TeamsProtectionPolicy, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_defender_zap_for_teams_enabled: + def test_zap_enabled_pass(self): + """Test PASS scenario when ZAP is enabled for Teams.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.teams_protection_policy = TeamsProtectionPolicy( + identity="Teams Protection Policy", + zap_enabled=True, + ) + + 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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_zap_for_teams_enabled.defender_zap_for_teams_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_zap_for_teams_enabled.defender_zap_for_teams_enabled import ( + defender_zap_for_teams_enabled, + ) + + check = defender_zap_for_teams_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Zero-hour auto purge (ZAP) is enabled for Microsoft Teams." + ) + assert result[0].resource == defender_client.teams_protection_policy.dict() + assert result[0].resource_name == "Teams Protection Policy" + assert result[0].resource_id == "teamsProtectionPolicy" + assert result[0].location == "global" + + def test_zap_disabled_fail(self): + """Test FAIL scenario when ZAP is disabled for Teams.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.teams_protection_policy = TeamsProtectionPolicy( + identity="Teams Protection Policy", + zap_enabled=False, + ) + + 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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_zap_for_teams_enabled.defender_zap_for_teams_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_zap_for_teams_enabled.defender_zap_for_teams_enabled import ( + defender_zap_for_teams_enabled, + ) + + check = defender_zap_for_teams_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Zero-hour auto purge (ZAP) is not enabled for Microsoft Teams." + ) + assert result[0].resource == defender_client.teams_protection_policy.dict() + assert result[0].resource_name == "Teams Protection Policy" + assert result[0].resource_id == "teamsProtectionPolicy" + assert result[0].location == "global" + + def test_teams_protection_policy_none(self): + """Test scenario when Teams protection policy is not available.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.teams_protection_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_zap_for_teams_enabled.defender_zap_for_teams_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_zap_for_teams_enabled.defender_zap_for_teams_enabled import ( + defender_zap_for_teams_enabled, + ) + + check = defender_zap_for_teams_enabled() + result = check.execute() + + assert len(result) == 0