feat(m365): add defender_zap_for_teams_enabled security check (#9838)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
This commit is contained in:
Andoni Alonso
2026-01-26 17:34:10 +01:00
committed by GitHub
parent ee93ad6cbc
commit 3d6aa6c650
10 changed files with 302 additions and 13 deletions
+1
View File
@@ -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)
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+13 -11
View File
@@ -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"
]
@@ -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.
@@ -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
@@ -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": ""
}
@@ -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
@@ -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