diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index fead633b6d..f32921b001 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -525,6 +525,26 @@ class M365PowerShell(PowerShellSession): "Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True ) + def get_inbound_spam_filter_rule(self) -> dict: + """ + Get Inbound Spam Filter Rule. + + Retrieves the current inbound spam filter rule settings for Exchange Online. + + Returns: + dict: Inbound spam filter rule settings in JSON format. + + Example: + >>> get_inbound_spam_filter_rule() + { + "Name": "Rule1", + "State": "Enabled" + } + """ + return self.execute( + "Get-HostedContentFilterRule | ConvertTo-Json", json_parse=True + ) + def get_report_submission_policy(self) -> dict: """ Get Exchange Online Report Submission Policy. diff --git a/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.py b/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.py index 18ac8f9949..61727b9d3c 100644 --- a/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.py +++ b/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.py @@ -23,37 +23,155 @@ class defender_antiphishing_policy_configured(Check): List[CheckReportM365]: A list of reports containing the result of the check. """ findings = [] - for policy_name, policy in defender_client.antiphishing_policies.items(): - report = CheckReportM365( - metadata=self.metadata(), - resource=policy, - resource_name="Defender Anti-Phishing Policy", - resource_id=policy_name, - ) - report.status = "FAIL" - report.status_extended = ( - f"Anti-phishing policy {policy_name} is not properly configured." - ) - if ( - not policy.default - and policy_name in defender_client.antiphising_rules - and defender_client.antiphising_rules[policy_name].state.lower() - == "enabled" - ) or policy.default: - if ( - policy.spoof_intelligence - and policy.spoof_intelligence_action.lower() == "quarantine" - and policy.dmarc_reject_action.lower() == "quarantine" - and policy.dmarc_quarantine_action.lower() == "quarantine" - and policy.safety_tips - and policy.unauthenticated_sender_action - and policy.show_tag - and policy.honor_dmarc_policy - ): + if defender_client.antiphishing_policies: + # Only Default Defender Anti-Phishing Policy exists since there are only anti phishing rules when there are custom policies + if not defender_client.antiphishing_rules: + # Get the only policy in the dictionary since there is only the default policy + policy = next(iter(defender_client.antiphishing_policies.values())) + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.name, + resource_id=policy.name, + ) + + if self._is_policy_properly_configured(policy): + # Case 1: Default policy exists and is properly configured report.status = "PASS" - report.status_extended = f"Anti-phishing policy {policy_name} is properly configured and enabled." + report.status_extended = f"{policy.name} is the only policy and it's properly configured in the default Defender Anti-Phishing Policy." + else: + # Case 5: Default policy exists but is not properly configured + report.status = "FAIL" + report.status_extended = f"{policy.name} is the only policy and it's not properly configured in the default Defender Anti-Phishing Policy." + findings.append(report) - findings.append(report) + # Multiple Defender Anti-Phishing Policies + else: + default_policy_well_configured = False + + for ( + policy_name, + policy, + ) in defender_client.antiphishing_policies.items(): + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy_name, + resource_id=policy_name, + ) + if policy.default: + if not self._is_policy_properly_configured(policy): + # Case 4: Default policy is not properly configured and there are other policies + report.status = "FAIL" + report.status_extended = f"{policy_name} is not properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another well-configured Custom Policy." + findings.append(report) + else: + # Case 2: Default policy is properly configured and there are other policies + report.status = "PASS" + report.status_extended = f"{policy_name} is properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another bad-configured Custom Policy." + default_policy_well_configured = True + findings.append(report) + else: + if not self._is_policy_properly_configured(policy): + included_resources = [] + + if defender_client.antiphishing_rules[policy.name].users: + included_resources.append( + f"users: {', '.join(defender_client.antiphishing_rules[policy.name].users)}" + ) + if defender_client.antiphishing_rules[policy.name].groups: + included_resources.append( + f"groups: {', '.join(defender_client.antiphishing_rules[policy.name].groups)}" + ) + if defender_client.antiphishing_rules[policy.name].domains: + included_resources.append( + f"domains: {', '.join(defender_client.antiphishing_rules[policy.name].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + # Case 3: Default policy is properly configured but other custom policies are not + if default_policy_well_configured: + report.status = "FAIL" + report.status_extended = ( + f"Custom Anti-phishing policy {policy_name} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). " + "However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + findings.append(report) + # Case 5: Default policy is not properly configured and other custom policies are not + else: + report.status = "FAIL" + report.status_extended = ( + f"Custom Anti-phishing policy {policy_name} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). " + "Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) + else: + included_resources = [] + + if defender_client.antiphishing_rules[policy.name].users: + included_resources.append( + f"users: {', '.join(defender_client.antiphishing_rules[policy.name].users)}" + ) + if defender_client.antiphishing_rules[policy.name].groups: + included_resources.append( + f"groups: {', '.join(defender_client.antiphishing_rules[policy.name].groups)}" + ) + if defender_client.antiphishing_rules[policy.name].domains: + included_resources.append( + f"domains: {', '.join(defender_client.antiphishing_rules[policy.name].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + # Case 2: Default policy is properly configured and other custom policies are too + if default_policy_well_configured: + report.status = "PASS" + report.status_extended = ( + f"Custom Anti-phishing policy {policy_name} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). " + "Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + findings.append(report) + + # Case 6: Default policy is not properly configured but other custom policies are + else: + report.status = "PASS" + report.status_extended = ( + f"Custom Anti-phishing policy {policy_name} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). " + "However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) return findings + + def _is_policy_properly_configured(self, policy) -> bool: + """ + Check if a policy is properly configured according to best practices. + + Args: + policy: The anti-phishing policy to check. + + Returns: + bool: True if the policy is properly configured, False otherwise. + """ + return ( + ( + policy.default + or defender_client.antiphishing_rules[policy.name].state.lower() + == "enabled" + ) + and policy.spoof_intelligence + and policy.spoof_intelligence_action.lower() == "quarantine" + and policy.dmarc_reject_action.lower() == "quarantine" + and policy.dmarc_quarantine_action.lower() == "quarantine" + and policy.safety_tips + and policy.unauthenticated_sender_action + and policy.show_tag + and policy.honor_dmarc_policy + ) diff --git a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.py b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.py index b4a0f2e31b..cd1eafdabf 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.py +++ b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.py @@ -6,8 +6,7 @@ from prowler.providers.m365.services.defender.defender_client import defender_cl class defender_antispam_outbound_policy_configured(Check): """ - Check if the Exchange Online Spam Policies are configured to notify administrators - when a sender is blocked for sending spam emails. + Check if the outbound spam policy is established and properly configured in the Defender service. Attributes: metadata: Metadata associated with the check (inherited from Check). @@ -15,40 +14,159 @@ class defender_antispam_outbound_policy_configured(Check): def execute(self) -> List[CheckReportM365]: """ - Execute the check to verify if the Exchange Online Spam Policies notify administrators - when a sender is blocked for sending spam emails. + Execute the check to verify if an outbound spam policy is established and properly configured. + + This method checks the Defender outbound spam policies to ensure they are configured + according to best practices. Returns: List[CheckReportM365]: A list of reports containing the result of the check. """ findings = [] - for policy_name, policy in defender_client.outbound_spam_policies.items(): - report = CheckReportM365( - metadata=self.metadata(), - resource=policy, - resource_name="Defender Outbound Spam Policy", - resource_id=policy_name, - ) - report.status = "FAIL" - report.status_extended = ( - f"Outbound Spam Policy {policy_name} is not properly configured." - ) - if ( - not policy.default - and policy_name in defender_client.outbound_spam_rules - and defender_client.outbound_spam_rules[policy_name].state.lower() - == "enabled" - ) or policy.default: - if ( - policy.notify_limit_exceeded - and policy.notify_sender_blocked - and policy.notify_limit_exceeded_addresses - and policy.notify_sender_blocked_addresses - ): + if defender_client.outbound_spam_policies: + # Only Default Defender Outbound Spam Policy + if not defender_client.outbound_spam_rules: + # Get the only policy in the dictionary + policy = next(iter(defender_client.outbound_spam_policies.values())) + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.name, + resource_id=policy.name, + ) + + if self._is_policy_properly_configured(policy): + # Case 1: Default policy exists and is properly configured report.status = "PASS" - report.status_extended = f"Outbound Spam Policy {policy_name} is properly configured and enabled." + report.status_extended = f"{policy.name} is the only policy and it's properly configured in the default Defender Outbound Spam Policy." + else: + # Case 5: Default policy exists but is not properly configured + report.status = "FAIL" + report.status_extended = f"{policy.name} is the only policy and it's not properly configured in the default Defender Outbound Spam Policy." + findings.append(report) - findings.append(report) + # Multiple Defender Outbound Spam Policies + else: + default_policy_well_configured = False + + for ( + policy_name, + policy, + ) in defender_client.outbound_spam_policies.items(): + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy_name, + resource_id=policy_name, + ) + if policy.default: + if not self._is_policy_properly_configured(policy): + # Case 4: Default policy is not properly configured and there are other policies + report.status = "FAIL" + report.status_extended = f"{policy_name} is not properly configured in the default Defender Outbound Spam Policy, but could be overridden by another well-configured Custom Policy." + findings.append(report) + else: + # Case 2: Default policy is properly configured and there are other policies + report.status = "PASS" + report.status_extended = f"{policy_name} is properly configured in the default Defender Outbound Spam Policy, but could be overridden by another bad-configured Custom Policy." + default_policy_well_configured = True + findings.append(report) + else: + if not self._is_policy_properly_configured(policy): + included_resources = [] + + if defender_client.outbound_spam_rules[policy.name].users: + included_resources.append( + f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}" + ) + if defender_client.outbound_spam_rules[policy.name].groups: + included_resources.append( + f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}" + ) + if defender_client.outbound_spam_rules[policy.name].domains: + included_resources.append( + f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + # Case 3: Default policy is properly configured but other custom policies are not + if default_policy_well_configured: + report.status = "FAIL" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + findings.append(report) + # Case 5: Default policy is not properly configured and other custom policies are not + else: + report.status = "FAIL" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) + else: + included_resources = [] + + if defender_client.outbound_spam_rules[policy.name].users: + included_resources.append( + f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}" + ) + if defender_client.outbound_spam_rules[policy.name].groups: + included_resources.append( + f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}" + ) + if defender_client.outbound_spam_rules[policy.name].domains: + included_resources.append( + f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + # Case 2: Default policy is properly configured and other custom policies are too + if default_policy_well_configured: + report.status = "PASS" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + findings.append(report) + # Case 6: Default policy is not properly configured but other custom policies are + else: + report.status = "PASS" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) return findings + + def _is_policy_properly_configured(self, policy) -> bool: + """ + Check if a policy is properly configured according to best practices. + + Args: + policy: The outbound spam policy to check. + + Returns: + bool: True if the policy is properly configured, False otherwise. + """ + return ( + ( + policy.default + or defender_client.outbound_spam_rules[policy.name].state.lower() + == "enabled" + ) + and policy.notify_limit_exceeded + and policy.notify_sender_blocked + and policy.notify_limit_exceeded_addresses + and policy.notify_sender_blocked_addresses + ) diff --git a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py index 8bb8c4fa60..04f81f62b3 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py +++ b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py @@ -20,28 +20,150 @@ class defender_antispam_outbound_policy_forwarding_disabled(Check): List[CheckReportM365]: A list of reports containing the result of the check. """ findings = [] - for policy_name, policy in defender_client.outbound_spam_policies.items(): - report = CheckReportM365( - metadata=self.metadata(), - resource=policy, - resource_name="Defender Outbound Spam Policy", - resource_id=policy_name, - ) - report.status = "FAIL" - report.status_extended = ( - f"Outbound Spam Policy {policy_name} does allow mail forwarding." - ) - if ( - not policy.default - and policy_name in defender_client.outbound_spam_rules - and defender_client.outbound_spam_rules[policy_name].state.lower() - == "enabled" - ) or policy.default: - if not policy.auto_forwarding_mode: + if defender_client.outbound_spam_policies: + # Only Default Defender Outbound Spam Policy exists + if not defender_client.outbound_spam_rules: + policy = next(iter(defender_client.outbound_spam_policies.values())) + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.name, + resource_id=policy.name, + ) + + if self._is_forwarding_disabled(policy): + # Case 1: Default policy exists and has forwarding disabled report.status = "PASS" - report.status_extended = f"Outbound Spam Policy {policy_name} does not allow mail forwarding." + report.status_extended = f"{policy.name} is the only policy and mail forwarding is disabled." + else: + # Case 5: Default policy exists but allows forwarding + report.status = "FAIL" + report.status_extended = f"{policy.name} is the only policy and mail forwarding is allowed." + findings.append(report) - findings.append(report) + # Multiple Defender Outbound Spam Policies exist + else: + default_policy_well_configured = False + + for ( + policy_name, + policy, + ) in defender_client.outbound_spam_policies.items(): + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy_name, + resource_id=policy_name, + ) + + if policy.default: + if not self._is_forwarding_disabled(policy): + # Case 4: Default policy allows forwarding and there are other policies + report.status = "FAIL" + report.status_extended = ( + f"{policy_name} is the default policy and mail forwarding is allowed, " + "but it could be overridden by another well-configured Custom Policy." + ) + findings.append(report) + else: + # Case 2: Default policy disables forwarding and there are other policies + report.status = "PASS" + report.status_extended = ( + f"{policy_name} is the default policy and mail forwarding is disabled, " + "but it could be overridden by another misconfigured Custom Policy." + ) + default_policy_well_configured = True + findings.append(report) + else: + if not self._is_forwarding_disabled(policy): + included_resources = [] + + if defender_client.outbound_spam_rules[policy.name].users: + included_resources.append( + f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}" + ) + if defender_client.outbound_spam_rules[policy.name].groups: + included_resources.append( + f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}" + ) + if defender_client.outbound_spam_rules[policy.name].domains: + included_resources.append( + f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + if default_policy_well_configured: + # Case 3: Default policy disables forwarding but custom one doesn't + report.status = "FAIL" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} allows mail forwarding and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "However, the default policy disables mail forwarding, so entities not included by this custom policy could be correctly protected." + ) + findings.append(report) + else: + # Case 5: Neither default nor custom policies disable forwarding + report.status = "FAIL" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} allows mail forwarding and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "Also, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) + else: + included_resources = [] + + if defender_client.outbound_spam_rules[policy.name].users: + included_resources.append( + f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}" + ) + if defender_client.outbound_spam_rules[policy.name].groups: + included_resources.append( + f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}" + ) + if defender_client.outbound_spam_rules[policy.name].domains: + included_resources.append( + f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + if default_policy_well_configured: + # Case 2: Both default and custom policies disable forwarding + report.status = "PASS" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} disables mail forwarding and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "Also, the default policy disables mail forwarding, so entities not included by this custom policy could still be correctly protected." + ) + findings.append(report) + else: + # Case 6: Default policy allows forwarding, custom policy disables it + report.status = "PASS" + report.status_extended = ( + f"Custom Outbound Spam policy {policy_name} disables mail forwarding and includes {included_resources_str}, " + f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). " + "However, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) return findings + + def _is_forwarding_disabled(self, policy) -> bool: + """ + Check if mail forwarding is disabled in the policy. + + Args: + policy: The outbound spam policy to check. + + Returns: + bool: True if mail forwarding is disabled, False otherwise. + """ + return ( + policy.default + or defender_client.outbound_spam_rules[policy.name].state.lower() + == "enabled" + ) and not policy.auto_forwarding_mode diff --git a/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py index 71dbb7e45f..67b6643e9e 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py +++ b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py @@ -23,20 +23,163 @@ class defender_antispam_policy_inbound_no_allowed_domains(Check): List[CheckReportM365]: A list of reports containing the result of the check. """ findings = [] - for policy in defender_client.inbound_spam_policies: - report = CheckReportM365( - metadata=self.metadata(), - resource=policy, - resource_name="Defender Inbound Spam Policy", - resource_id=policy.identity, - ) - report.status = "PASS" - report.status_extended = f"Inbound anti-spam policy {policy.identity} does not contain allowed domains." - if policy.allowed_sender_domains: - report.status = "FAIL" - report.status_extended = f"Inbound anti-spam policy {policy.identity} contains allowed domains: {policy.allowed_sender_domains}." + if defender_client.inbound_spam_policies: + # Only Default Defender Inbound Spam Policy exists + if not defender_client.inbound_spam_rules: + policy = defender_client.inbound_spam_policies[0] - findings.append(report) + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.identity, + resource_id=policy.identity, + ) + + if self._has_no_allowed_domains(policy): + # Case 1: Default policy exists and has no allowed domains + report.status = "PASS" + report.status_extended = f"{policy.identity} is the only policy and it does not contain allowed domains." + else: + # Case 5: Default policy exists but contains allowed domains + report.status = "FAIL" + report.status_extended = f"{policy.identity} is the only policy and it contains allowed domains: {', '.join(policy.allowed_sender_domains)}." + findings.append(report) + + # Multiple Defender Inbound Spam Policies exist + else: + default_policy_well_configured = False + + for policy in defender_client.inbound_spam_policies: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.identity, + resource_id=policy.identity, + ) + + if policy.default: + if not self._has_no_allowed_domains(policy): + # Case 4: Default policy contains allowed domains + report.status = "FAIL" + report.status_extended = ( + f"{policy.identity} is the default policy and it contains allowed domains: {', '.join(policy.allowed_sender_domains)}, " + "but it could be overridden by another well-configured Custom Policy." + ) + findings.append(report) + else: + # Case 2: Default policy has no allowed domains and there are other policies + report.status = "PASS" + report.status_extended = ( + f"{policy.identity} is the default policy and it does not contain allowed domains, " + "but it could be overridden by another misconfigured Custom Policy." + ) + default_policy_well_configured = True + findings.append(report) + else: + if not self._has_no_allowed_domains(policy): + included_resources = [] + + if defender_client.inbound_spam_rules[ + policy.identity + ].users: + included_resources.append( + f"users: {', '.join(defender_client.inbound_spam_rules[policy.identity].users)}" + ) + if defender_client.inbound_spam_rules[ + policy.identity + ].groups: + included_resources.append( + f"groups: {', '.join(defender_client.inbound_spam_rules[policy.identity].groups)}" + ) + if defender_client.inbound_spam_rules[ + policy.identity + ].domains: + included_resources.append( + f"domains: {', '.join(defender_client.inbound_spam_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + priority = defender_client.inbound_spam_rules[ + policy.identity + ].priority + + if default_policy_well_configured: + # Case 3: Default policy has no allowed domains but custom one does + report.status = "FAIL" + report.status_extended = ( + f"Custom Inbound Spam policy {policy.identity} contains allowed domains and includes {included_resources_str}, " + f"with priority {priority} (0 is the highest). However, the default policy does not contain allowed domains, " + "so entities not included by this custom policy could be correctly protected." + ) + else: + # Case 5: Neither default nor custom policies are correctly configured + report.status = "FAIL" + report.status_extended = ( + f"Custom Inbound Spam policy {policy.identity} contains allowed domains and includes {included_resources_str}, " + f"with priority {priority} (0 is the highest). Also, the default policy contains allowed domains, " + "so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) + else: + included_resources = [] + + if defender_client.inbound_spam_rules[ + policy.identity + ].users: + included_resources.append( + f"users: {', '.join(defender_client.inbound_spam_rules[policy.identity].users)}" + ) + if defender_client.inbound_spam_rules[ + policy.identity + ].groups: + included_resources.append( + f"groups: {', '.join(defender_client.inbound_spam_rules[policy.identity].groups)}" + ) + if defender_client.inbound_spam_rules[ + policy.identity + ].domains: + included_resources.append( + f"domains: {', '.join(defender_client.inbound_spam_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + priority = defender_client.inbound_spam_rules[ + policy.identity + ].priority + + if default_policy_well_configured: + # Case 2: Both default and custom policies do not contain allowed domains + report.status = "PASS" + report.status_extended = ( + f"Custom Inbound Spam policy {policy.identity} does not contain allowed domains and includes {included_resources_str}, " + f"with priority {priority} (0 is the highest). Also, the default policy does not contain allowed domains, " + "so entities not included by this custom policy could still be correctly protected." + ) + else: + # Case 6: Default policy contains allowed domains, custom policy does not + report.status = "PASS" + report.status_extended = ( + f"Custom Inbound Spam policy {policy.identity} does not contain allowed domains and includes {included_resources_str}, " + f"with priority {priority} (0 is the highest). However, the default policy contains allowed domains, " + "so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) return findings + + def _has_no_allowed_domains(self, policy) -> bool: + """ + Check if the policy has no allowed domains. + + Args: + policy: The inbound spam policy to check. + + Returns: + bool: True if the policy has no allowed domains, False otherwise. + """ + return ( + policy.default + or defender_client.inbound_spam_rules[policy.identity].state.lower() + == "enabled" + ) and not policy.allowed_sender_domains diff --git a/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.py b/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.py index 75c9707d26..a0b817cda3 100644 --- a/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.py +++ b/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.py @@ -16,39 +16,138 @@ class defender_malware_policy_common_attachments_filter_enabled(Check): """ Execute the check to verify if the Common Attachment Types Filter is enabled. - This method checks the Defender anti-malware policy to determine if the + This method checks the Defender anti-malware policies to determine if the Common Attachment Types Filter is enabled. Returns: List[CheckReportM365]: A list of reports containing the result of the check. """ findings = [] - if not defender_client.malware_policies: - report = CheckReportM365( - metadata=self.metadata(), - resource={}, - resource_name="Defender Malware Policy", - resource_id="defenderMalwarePolicy", - ) - report.status = "FAIL" - report.status_extended = "Common Attachment Types Filter is not enabled." - findings.append(report) - else: - for policy in defender_client.malware_policies: + + if defender_client.malware_policies: + # Only Default Defender Malware Policy exists + if not defender_client.malware_rules: + policy = defender_client.malware_policies[0] + report = CheckReportM365( metadata=self.metadata(), resource=policy, - resource_name="Defender Malware Policy", - resource_id="defenderMalwarePolicy", + resource_name=policy.identity, + resource_id=policy.identity, ) - report.status = "FAIL" - report.status_extended = f"Common Attachment Types Filter is not enabled in anti-malware policy {policy.identity}." if policy.enable_file_filter: + # Case 1: Default policy exists and has the setting enabled report.status = "PASS" - report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}." - break + report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is enabled." + else: + # Case 5: Default policy exists but doesn't have the setting enabled + report.status = "FAIL" + report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is not enabled." + findings.append(report) - findings.append(report) + # Multiple Defender Malware Policies exist + else: + default_policy_well_configured = False + + for policy in defender_client.malware_policies: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.identity, + resource_id=policy.identity, + ) + + if policy.is_default: + if not policy.enable_file_filter: + # Case 4: Default policy doesn't have the setting enabled and there are other policies + report.status = "FAIL" + report.status_extended = ( + f"{policy.identity} is the default policy and Common Attachment Types Filter is not enabled, " + "but it could be overridden by another well-configured Custom Policy." + ) + findings.append(report) + else: + # Case 2: Default policy has the setting enabled and there are other policies + report.status = "PASS" + report.status_extended = ( + f"{policy.identity} is the default policy and Common Attachment Types Filter is enabled, " + "but it could be overridden by another misconfigured Custom Policy." + ) + default_policy_well_configured = True + findings.append(report) + else: + if not policy.enable_file_filter: + included_resources = [] + + if defender_client.malware_rules[policy.identity].users: + included_resources.append( + f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}" + ) + if defender_client.malware_rules[policy.identity].groups: + included_resources.append( + f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}" + ) + if defender_client.malware_rules[policy.identity].domains: + included_resources.append( + f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + if default_policy_well_configured: + # Case 3: Default policy enables the setting but custom one doesn't + report.status = "FAIL" + report.status_extended = ( + f"Custom Malware policy {policy.identity} does not enable Common Attachment Types Filter and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "However, the default policy enables the filter, so entities not included by this custom policy could be correctly protected." + ) + findings.append(report) + else: + # Case 5: Neither default nor custom policies enable the setting + report.status = "FAIL" + report.status_extended = ( + f"Custom Malware policy {policy.identity} does not enable Common Attachment Types Filter and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "Also, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) + else: + included_resources = [] + + if defender_client.malware_rules[policy.identity].users: + included_resources.append( + f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}" + ) + if defender_client.malware_rules[policy.identity].groups: + included_resources.append( + f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}" + ) + if defender_client.malware_rules[policy.identity].domains: + included_resources.append( + f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + if default_policy_well_configured: + # Case 2: Both default and custom policies enable the setting + report.status = "PASS" + report.status_extended = ( + f"Custom Malware policy {policy.identity} enables Common Attachment Types Filter and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "Also, the default policy enables the filter, so entities not included by this custom policy could still be correctly protected." + ) + findings.append(report) + else: + # Case 6: Default policy doesn't enable the setting, but custom policy does + report.status = "PASS" + report.status_extended = ( + f"Custom Malware policy {policy.identity} enables Common Attachment Types Filter and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "However, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) return findings diff --git a/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.py b/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.py index cd2682d538..87aa4ab50e 100644 --- a/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.py +++ b/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.py @@ -14,17 +14,16 @@ class defender_malware_policy_comprehensive_attachments_filter_applied(Check): def execute(self) -> List[CheckReportM365]: """ - Executes the check to determine if the Common Attachment Types Filter is enabled. + Executes the check to determine if the Common Attachment Types Filter is properly configured. - This method evaluates the Defender anti-malware policy to ensure it is enabled and the - Common Attachment Types Filter is active and applied to the recommended file types. + This method evaluates the Defender anti-malware policies to ensure the filter is enabled and applied + to all recommended file types. Returns: List[CheckReportM365]: A list of reports with the results of the check. """ findings = [] - # Default to Microsoft-recommended common file types default_recommended_extensions = [ "ace", "ani", @@ -81,63 +80,175 @@ class defender_malware_policy_comprehensive_attachments_filter_applied(Check): "z", ] - # Load extensions from audit_config (user config), fallback to default recommended_extensions = defender_client.audit_config.get( "recommended_blocked_file_types", default_recommended_extensions ) - if not defender_client.malware_policies: - report = CheckReportM365( - metadata=self.metadata(), - resource={}, - resource_name="Defender Malware Policy", - resource_id="defenderMalwarePolicy", - ) - report.status = "FAIL" - report.status_extended = "Common Attachment Types Filter is not enabled." - findings.append(report) - else: - for policy in defender_client.malware_policies: + if defender_client.malware_policies: + # Only Default Defender Malware Policy exists + if not defender_client.malware_rules: + policy = defender_client.malware_policies[0] + report = CheckReportM365( metadata=self.metadata(), resource=policy, - resource_name="Defender Malware Policy", - resource_id="defenderMalwarePolicy", + resource_name=policy.identity, + resource_id=policy.identity, ) - report.status = "FAIL" - report.status_extended = f"Common Attachment Types Filter is not properly configured in anti-malware policy {policy.identity}." - if not policy.enable_file_filter: - report.status_extended = f"Common Attachment Types Filter is not enabled in anti-malware policy {policy.identity}." - break + if self._is_filter_properly_configured(policy, recommended_extensions): + # Case 1: Default policy exists and has filter properly configured + report.status = "PASS" + report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is properly configured." + else: + # Case 5: Default policy exists but doesn't have filter properly configured + missing = self._get_missing_extensions( + policy, recommended_extensions + ) + report.status = "FAIL" + report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is not properly configured. Missing recommended file types: {', '.join(missing)}." + findings.append(report) - if ( - not policy.is_default - and policy.identity in defender_client.malware_rules - ) or policy.is_default: - if ( - not policy.is_default - and defender_client.malware_rules[policy.identity].state.lower() - == "enabled" - ) or policy.is_default: - blocked_extensions = [ext.lower() for ext in policy.file_types] - missing = [ - ext - for ext in recommended_extensions - if ext.lower() not in blocked_extensions - ] + # Multiple Defender Malware Policies exist + else: + default_policy_well_configured = False - if missing: - report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}, but the following recommended file types are missing: {', '.join(missing)}." - break - - report.status = "PASS" - report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}, the policy is enabled and the filter is applied to the recommended file types." + for policy in defender_client.malware_policies: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.identity, + resource_id=policy.identity, + ) + if policy.is_default: + if not self._is_filter_properly_configured( + policy, recommended_extensions + ): + # Case 4: Default policy is not properly configured + missing = self._get_missing_extensions( + policy, recommended_extensions + ) + report.status = "FAIL" + report.status_extended = ( + f"{policy.identity} is the default policy and Common Attachment Types Filter is not properly configured, " + f"but it could be overridden by another well-configured Custom Policy. Missing recommended file types: {', '.join(missing)}." + ) + findings.append(report) + else: + # Case 2: Default policy is properly configured + report.status = "PASS" + report.status_extended = ( + f"{policy.identity} is the default policy and Common Attachment Types Filter is properly configured, " + "but it could be overridden by another misconfigured Custom Policy." + ) + default_policy_well_configured = True + findings.append(report) else: - report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}, but the policy is disabled." - break + if not self._is_filter_properly_configured( + policy, recommended_extensions + ): + included_resources = [] - findings.append(report) + if defender_client.malware_rules[policy.identity].users: + included_resources.append( + f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}" + ) + if defender_client.malware_rules[policy.identity].groups: + included_resources.append( + f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}" + ) + if defender_client.malware_rules[policy.identity].domains: + included_resources.append( + f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + missing = self._get_missing_extensions( + policy, recommended_extensions + ) + + if default_policy_well_configured: + # Case 3: Default policy is configured, custom one isn't + report.status = "FAIL" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + f"Missing recommended file types: {', '.join(missing)}. " + "However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + findings.append(report) + else: + # Case 5: Neither default nor custom policy is properly configured + report.status = "FAIL" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + f"Missing recommended file types: {', '.join(missing)}. " + "Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) + else: + included_resources = [] + + if defender_client.malware_rules[policy.identity].users: + included_resources.append( + f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}" + ) + if defender_client.malware_rules[policy.identity].groups: + included_resources.append( + f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}" + ) + if defender_client.malware_rules[policy.identity].domains: + included_resources.append( + f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + if default_policy_well_configured: + # Case 2: Both default and custom policies are properly configured + report.status = "PASS" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + findings.append(report) + else: + # Case 6: Default policy not configured, custom policy is + report.status = "PASS" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) return findings + + def _is_filter_properly_configured(self, policy, recommended_extensions) -> bool: + if not policy.enable_file_filter: + return False + + if ( + not policy.is_default + and policy.identity in defender_client.malware_rules + and defender_client.malware_rules[policy.identity].state.lower() + != "enabled" + ): + return False + + blocked_extensions = [ext.lower() for ext in policy.file_types] + return all(ext.lower() in blocked_extensions for ext in recommended_extensions) + + def _get_missing_extensions(self, policy, recommended_extensions) -> List[str]: + if not policy.enable_file_filter: + return recommended_extensions + + blocked_extensions = [ext.lower() for ext in policy.file_types] + return [ + ext + for ext in recommended_extensions + if ext.lower() not in blocked_extensions + ] diff --git a/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.py b/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.py index 6eaf933c6a..735cc6b9e9 100644 --- a/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.py +++ b/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.py @@ -16,47 +16,152 @@ class defender_malware_policy_notifications_internal_users_malware_enabled(Check """ Execute the check to verify if notifications for internal users sending malware are enabled. - This method checks the Defender anti-malware policy to determine if notifications for internal users - sending malware are enabled. + This method evaluates the Defender anti-malware policies to ensure internal sender notifications + are properly configured. Returns: - List[CheckReportM365]: A list of reports containing the result of the check. + List[CheckReportM365]: A list of reports with the results of the check. """ findings = [] - if not defender_client.malware_policies: - report = CheckReportM365( - metadata=self.metadata(), - resource={}, - resource_name="Defender Malware Policy", - resource_id="defenderMalwarePolicy", - ) - report.status = "FAIL" - report.status_extended = ( - "Notifications for internal users sending malware are not enabled." - ) - findings.append(report) - else: - for policy in defender_client.malware_policies: + + if defender_client.malware_policies: + # Only Default Defender Malware Policy exists + if not defender_client.malware_rules: + policy = defender_client.malware_policies[0] + report = CheckReportM365( metadata=self.metadata(), resource=policy, - resource_name="Defender Malware Policy", - resource_id="defenderMalwarePolicy", - ) - report.status = "FAIL" - report.status_extended = ( - "Notifications for internal users sending malware are not enabled." + resource_name=policy.identity, + resource_id=policy.identity, ) - if policy.enable_internal_sender_admin_notifications: - if policy.internal_sender_admin_address: - report.status = "PASS" - report.status_extended = "Notifications for internal users sending malware are enabled." - break + if self._are_notifications_enabled(policy): + # Case 1: Default policy exists and has notifications enabled + report.status = "PASS" + report.status_extended = f"{policy.identity} is the only policy and notifications for internal users sending malware are enabled." + else: + # Case 5: Default policy exists but doesn't have notifications enabled + report.status = "FAIL" + report.status_extended = f"{policy.identity} is the only policy and notifications for internal users sending malware are not enabled." + findings.append(report) + + # Multiple Defender Malware Policies exist + else: + default_policy_well_configured = False + + for policy in defender_client.malware_policies: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.identity, + resource_id=policy.identity, + ) + + if policy.is_default: + if not self._are_notifications_enabled(policy): + # Case 4: Default policy not configured + report.status = "FAIL" + report.status_extended = ( + f"{policy.identity} is the default policy and notifications for internal users sending malware are not enabled, " + "but it could be overridden by another well-configured Custom Policy." + ) + findings.append(report) + else: + # Case 2: Default policy is properly configured + report.status = "PASS" + report.status_extended = ( + f"{policy.identity} is the default policy and notifications for internal users sending malware are enabled, " + "but it could be overridden by another misconfigured Custom Policy." + ) + default_policy_well_configured = True + findings.append(report) else: - report.status = "FAIL" - report.status_extended = "Notifications for internal users sending malware are enabled, but no email addresses are configured." + if not self._are_notifications_enabled(policy): + included_resources = [] - findings.append(report) + if defender_client.malware_rules[policy.identity].users: + included_resources.append( + f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}" + ) + if defender_client.malware_rules[policy.identity].groups: + included_resources.append( + f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}" + ) + if defender_client.malware_rules[policy.identity].domains: + included_resources.append( + f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + if default_policy_well_configured: + # Case 3: Default policy is configured, custom one isn't + report.status = "FAIL" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + findings.append(report) + else: + # Case 5: Neither default nor custom policy is properly configured + report.status = "FAIL" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) + else: + included_resources = [] + + if defender_client.malware_rules[policy.identity].users: + included_resources.append( + f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}" + ) + if defender_client.malware_rules[policy.identity].groups: + included_resources.append( + f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}" + ) + if defender_client.malware_rules[policy.identity].domains: + included_resources.append( + f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}" + ) + + included_resources_str = "; ".join(included_resources) + + if default_policy_well_configured: + # Case 2: Both default and custom policies are properly configured + report.status = "PASS" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + findings.append(report) + else: + # Case 6: Default policy not configured, custom policy is + report.status = "PASS" + report.status_extended = ( + f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, " + f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). " + "However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + findings.append(report) return findings + + def _are_notifications_enabled(self, policy) -> bool: + if ( + not policy.is_default + and policy.identity in defender_client.malware_rules + and defender_client.malware_rules[policy.identity].state.lower() + != "enabled" + ): + return False + + return ( + policy.enable_internal_sender_admin_notifications + and policy.internal_sender_admin_address + ) diff --git a/prowler/providers/m365/services/defender/defender_service.py b/prowler/providers/m365/services/defender/defender_service.py index cec2e7dce4..9ea4a64afd 100644 --- a/prowler/providers/m365/services/defender/defender_service.py +++ b/prowler/providers/m365/services/defender/defender_service.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from pydantic import BaseModel @@ -14,10 +14,11 @@ class Defender(M365Service): self.outbound_spam_policies = {} self.outbound_spam_rules = {} self.antiphishing_policies = {} - self.antiphising_rules = {} + self.antiphishing_rules = {} self.connection_filter_policy = None self.dkim_configurations = [] self.inbound_spam_policies = [] + self.inbound_spam_rules = {} self.report_submission_policy = None if self.powershell: self.powershell.connect_exchange_online() @@ -25,11 +26,12 @@ class Defender(M365Service): self.malware_rules = self._get_malware_filter_rule() self.outbound_spam_policies = self._get_outbound_spam_filter_policy() self.outbound_spam_rules = self._get_outbound_spam_filter_rule() - self.antiphishing_policies = self._get_antiphising_policy() - self.antiphising_rules = self._get_antiphising_rules() + self.antiphishing_policies = self._get_antiphishing_policy() + self.antiphishing_rules = self._get_antiphishing_rules() self.connection_filter_policy = self._get_connection_filter_policy() self.dkim_configurations = self._get_dkim_config() 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.powershell.close() @@ -44,7 +46,7 @@ class Defender(M365Service): if policy: malware_policies.append( MalwarePolicy( - enable_file_filter=policy.get("EnableFileFilter", True), + enable_file_filter=policy.get("EnableFileFilter", False), identity=policy.get("Identity", ""), enable_internal_sender_admin_notifications=policy.get( "EnableInternalSenderAdminNotifications", False @@ -56,6 +58,7 @@ class Defender(M365Service): is_default=policy.get("IsDefault", False), ) ) + malware_policies.sort(key=lambda x: x.is_default, reverse=True) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -73,6 +76,10 @@ class Defender(M365Service): if rule: malware_rules[rule.get("Name", "")] = MalwareRule( state=rule.get("State", ""), + priority=rule.get("Priority", 0), + users=rule.get("SentTo", None), + groups=rule.get("SentToMemberOf", None), + domains=rule.get("RecipientDomainIs", None), ) except Exception as error: logger.error( @@ -80,7 +87,7 @@ class Defender(M365Service): ) return malware_rules - def _get_antiphising_policy(self): + def _get_antiphishing_policy(self): logger.info("Microsoft365 - Getting Defender antiphishing policy...") antiphishing_policies = {} try: @@ -90,6 +97,7 @@ class Defender(M365Service): for policy in antiphishing_policy: if policy: antiphishing_policies[policy.get("Name", "")] = AntiphishingPolicy( + name=policy.get("Name", ""), spoof_intelligence=policy.get("EnableSpoofIntelligence", True), spoof_intelligence_action=policy.get( "AuthenticationFailAction", "" @@ -104,13 +112,21 @@ class Defender(M365Service): honor_dmarc_policy=policy.get("HonorDmarcPolicy", True), default=policy.get("IsDefault", False), ) + + antiphishing_policies = dict( + sorted( + antiphishing_policies.items(), + key=lambda item: item[1].default, + reverse=True, + ) + ) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return antiphishing_policies - def _get_antiphising_rules(self): + def _get_antiphishing_rules(self): logger.info("Microsoft365 - Getting Defender antiphishing rules...") antiphishing_rules = {} try: @@ -121,6 +137,10 @@ class Defender(M365Service): if rule: antiphishing_rules[rule.get("Name", "")] = AntiphishingRule( state=rule.get("State", ""), + priority=rule.get("Priority", 0), + users=rule.get("SentTo", None), + groups=rule.get("SentToMemberOf", None), + domains=rule.get("RecipientDomainIs", None), ) except Exception as error: logger.error( @@ -176,6 +196,7 @@ class Defender(M365Service): for policy in outbound_spam_policy: if policy: outbound_spam_policies[policy.get("Name", "")] = OutboundSpamPolicy( + name=policy.get("Name", ""), notify_sender_blocked=policy.get("NotifyOutboundSpam", True), notify_limit_exceeded=policy.get( "BccSuspiciousOutboundMail", True @@ -189,6 +210,14 @@ class Defender(M365Service): auto_forwarding_mode=policy.get("AutoForwardingMode", True), default=policy.get("IsDefault", False), ) + + outbound_spam_policies = dict( + sorted( + outbound_spam_policies.items(), + key=lambda item: item[1].default, + reverse=True, + ) + ) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -206,6 +235,10 @@ class Defender(M365Service): if rule: outbound_spam_rules[rule.get("Name", "")] = OutboundSpamRule( state=rule.get("State", "Disabled"), + priority=rule.get("Priority", 0), + users=rule.get("From", None), + groups=rule.get("FromMemberOf", None), + domains=rule.get("SenderDomainIs", None), ) except Exception as error: logger.error( @@ -230,14 +263,38 @@ class Defender(M365Service): allowed_sender_domains=policy.get( "AllowedSenderDomains", [] ), + default=policy.get("IsDefault", False), ) ) + inbound_spam_policies.sort(key=lambda x: x.default, reverse=True) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return inbound_spam_policies + def _get_inbound_spam_filter_rule(self): + logger.info("Microsoft365 - Getting Defender inbound spam filter rule...") + inbound_spam_rules = {} + try: + inbound_spam_rule = self.powershell.get_inbound_spam_filter_rule() + if isinstance(inbound_spam_rule, dict): + inbound_spam_rule = [inbound_spam_rule] + for rule in inbound_spam_rule: + if rule: + inbound_spam_rules[rule.get("Name", "")] = InboundSpamRule( + state=rule.get("State", "Disabled"), + priority=rule.get("Priority", 0), + users=rule.get("SentTo", None), + groups=rule.get("SentToMemberOf", None), + domains=rule.get("RecipientDomainIs", None), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return inbound_spam_rules + def _get_report_submission_policy(self): logger.info("Microsoft365 - Getting Defender report submission policy...") report_submission_policy = None @@ -288,9 +345,14 @@ class MalwarePolicy(BaseModel): class MalwareRule(BaseModel): state: str + priority: int + users: Optional[list[str]] + groups: Optional[list[str]] + domains: Optional[list[str]] class AntiphishingPolicy(BaseModel): + name: str spoof_intelligence: bool spoof_intelligence_action: str dmarc_reject_action: str @@ -304,6 +366,10 @@ class AntiphishingPolicy(BaseModel): class AntiphishingRule(BaseModel): state: str + priority: int + users: Optional[list[str]] + groups: Optional[list[str]] + domains: Optional[list[str]] class ConnectionFilterPolicy(BaseModel): @@ -318,6 +384,7 @@ class DkimConfig(BaseModel): class OutboundSpamPolicy(BaseModel): + name: str notify_sender_blocked: bool notify_limit_exceeded: bool notify_limit_exceeded_addresses: List[str] @@ -328,11 +395,24 @@ class OutboundSpamPolicy(BaseModel): class OutboundSpamRule(BaseModel): state: str + priority: int + users: Optional[list[str]] + groups: Optional[list[str]] + domains: Optional[list[str]] class DefenderInboundSpamPolicy(BaseModel): identity: str allowed_sender_domains: list[str] = [] + default: bool + + +class InboundSpamRule(BaseModel): + state: str + priority: int + users: Optional[list[str]] + groups: Optional[list[str]] + domains: Optional[list[str]] class ReportSubmissionPolicy(BaseModel): diff --git a/tests/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured_test.py b/tests/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured_test.py index b460646dae..8610e96769 100644 --- a/tests/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured_test.py +++ b/tests/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured_test.py @@ -4,7 +4,69 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider class Test_defender_antiphishing_policy_configured: - def test_properly_configured_custom_policy(self): + def test_case_1_default_policy_properly_configured(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import ( + defender_antiphishing_policy_configured, + ) + from prowler.providers.m365.services.defender.defender_service import ( + AntiphishingPolicy, + ) + + defender_client.antiphishing_policies = { + "Default": AntiphishingPolicy( + name="Default", + spoof_intelligence=True, + spoof_intelligence_action="Quarantine", + dmarc_reject_action="Quarantine", + dmarc_quarantine_action="Quarantine", + safety_tips=True, + unauthenticated_sender_action=True, + show_tag=True, + honor_dmarc_policy=True, + default=True, + ) + } + defender_client.antiphishing_rules = {} + + check = defender_antiphishing_policy_configured() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the only policy and it's properly configured in the default Defender Anti-Phishing Policy." + ) + assert ( + result[0].resource_name + == defender_client.antiphishing_policies["Default"].name + ) + assert ( + result[0].resource_id + == defender_client.antiphishing_policies["Default"].name + ) + assert ( + result[0].resource + == defender_client.antiphishing_policies["Default"].dict() + ) + + def test_case_2_all_policies_properly_configured(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -31,7 +93,20 @@ class Test_defender_antiphishing_policy_configured: ) defender_client.antiphishing_policies = { + "Default": AntiphishingPolicy( + name="Default", + spoof_intelligence=True, + spoof_intelligence_action="Quarantine", + dmarc_reject_action="Quarantine", + dmarc_quarantine_action="Quarantine", + safety_tips=True, + unauthenticated_sender_action=True, + show_tag=True, + honor_dmarc_policy=True, + default=True, + ), "Policy1": AntiphishingPolicy( + name="Policy1", spoof_intelligence=True, spoof_intelligence_action="Quarantine", dmarc_reject_action="Quarantine", @@ -41,29 +116,59 @@ class Test_defender_antiphishing_policy_configured: show_tag=True, honor_dmarc_policy=True, default=False, - ) + ), } - defender_client.antiphising_rules = { - "Policy1": AntiphishingRule(state="Enabled") + defender_client.antiphishing_rules = { + "Policy1": AntiphishingRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], + ) } check = defender_antiphishing_policy_configured() result = check.execute() - assert len(result) == 1 + assert len(result) == 2 assert result[0].status == "PASS" assert ( result[0].status_extended - == "Anti-phishing policy Policy1 is properly configured and enabled." + == "Default is properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another bad-configured Custom Policy." + ) + assert ( + result[0].resource_name + == defender_client.antiphishing_policies["Default"].name + ) + assert ( + result[0].resource_id + == defender_client.antiphishing_policies["Default"].name ) assert ( result[0].resource + == defender_client.antiphishing_policies["Default"].dict() + ) + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == f"Custom Anti-phishing policy {defender_client.antiphishing_policies['Policy1'].name} is properly configured and includes users: {', '.join(defender_client.antiphishing_rules['Policy1'].users)}; groups: {', '.join(defender_client.antiphishing_rules['Policy1'].groups)}; domains: {', '.join(defender_client.antiphishing_rules['Policy1'].domains)}, " + f"with priority {defender_client.antiphishing_rules['Policy1'].priority} (0 is the highest). " + "Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + assert ( + result[1].resource_name + == defender_client.antiphishing_policies["Policy1"].name + ) + assert ( + result[1].resource_id + == defender_client.antiphishing_policies["Policy1"].name + ) + assert ( + result[1].resource == defender_client.antiphishing_policies["Policy1"].dict() ) - assert result[0].resource_name == "Defender Anti-Phishing Policy" - assert result[0].resource_id == "Policy1" - assert result[0].location == "global" - def test_not_properly_configured_policy(self): + def test_case_3_default_ok_others_not(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -89,66 +194,9 @@ class Test_defender_antiphishing_policy_configured: AntiphishingRule, ) - defender_client.antiphishing_policies = { - "Policy2": AntiphishingPolicy( - spoof_intelligence=False, - spoof_intelligence_action="None", - dmarc_reject_action="None", - dmarc_quarantine_action="None", - safety_tips=False, - unauthenticated_sender_action=False, - show_tag=False, - honor_dmarc_policy=False, - default=False, - ) - } - defender_client.antiphising_rules = { - "Policy2": AntiphishingRule(state="Enabled") - } - - check = defender_antiphishing_policy_configured() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Anti-phishing policy Policy2 is not properly configured." - ) - assert ( - result[0].resource - == defender_client.antiphishing_policies["Policy2"].dict() - ) - assert result[0].resource_name == "Defender Anti-Phishing Policy" - assert result[0].resource_id == "Policy2" - assert result[0].location == "global" - - def test_properly_configured_default_policy(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import ( - defender_antiphishing_policy_configured, - ) - from prowler.providers.m365.services.defender.defender_service import ( - AntiphishingPolicy, - ) - defender_client.antiphishing_policies = { "Default": AntiphishingPolicy( + name="Default", spoof_intelligence=True, spoof_intelligence_action="Quarantine", dmarc_reject_action="Quarantine", @@ -158,27 +206,149 @@ class Test_defender_antiphishing_policy_configured: show_tag=True, honor_dmarc_policy=True, default=True, + ), + "Policy1": AntiphishingPolicy( + name="Policy1", + spoof_intelligence=False, + spoof_intelligence_action="None", + dmarc_reject_action="None", + dmarc_quarantine_action="None", + safety_tips=False, + unauthenticated_sender_action=False, + show_tag=False, + honor_dmarc_policy=False, + default=False, + ), + } + defender_client.antiphishing_rules = { + "Policy1": AntiphishingRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], ) } - defender_client.antiphising_rules = {} check = defender_antiphishing_policy_configured() result = check.execute() - assert len(result) == 1 + assert len(result) == 2 assert result[0].status == "PASS" assert ( result[0].status_extended - == "Anti-phishing policy Default is properly configured and enabled." + == "Default is properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another bad-configured Custom Policy." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert ( result[0].resource == defender_client.antiphishing_policies["Default"].dict() ) - assert result[0].resource_name == "Defender Anti-Phishing Policy" - assert result[0].resource_id == "Default" - assert result[0].location == "global" - def test_default_policy_not_properly_configured(self): + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Anti-phishing policy Policy1 is not properly configured and includes users: test@example.com; groups: example_group; domains: example.com, " + "with priority 1 (0 is the highest). However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.antiphishing_policies["Policy1"].dict() + ) + + def test_case_4_default_not_ok_potential_false_positive(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import ( + defender_antiphishing_policy_configured, + ) + from prowler.providers.m365.services.defender.defender_service import ( + AntiphishingPolicy, + AntiphishingRule, + ) + + defender_client.antiphishing_policies = { + "Default": AntiphishingPolicy( + name="Default", + spoof_intelligence=False, + spoof_intelligence_action="None", + dmarc_reject_action="None", + dmarc_quarantine_action="None", + safety_tips=False, + unauthenticated_sender_action=False, + show_tag=False, + honor_dmarc_policy=False, + default=True, + ), + "Policy1": AntiphishingPolicy( + name="Policy1", + spoof_intelligence=True, + spoof_intelligence_action="Quarantine", + dmarc_reject_action="Quarantine", + dmarc_quarantine_action="Quarantine", + safety_tips=True, + unauthenticated_sender_action=True, + show_tag=True, + honor_dmarc_policy=True, + default=False, + ), + } + defender_client.antiphishing_rules = { + "Policy1": AntiphishingRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], + ) + } + + check = defender_antiphishing_policy_configured() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is not properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.antiphishing_policies["Default"].dict() + ) + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Anti-phishing policy Policy1 is properly configured and includes users: test@example.com; groups: example_group; domains: example.com, " + f"with priority {defender_client.antiphishing_rules['Policy1'].priority} (0 is the highest). " + "However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.antiphishing_policies["Policy1"].dict() + ) + + def test_case_5_default_policy_not_properly_configured(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -205,6 +375,7 @@ class Test_defender_antiphishing_policy_configured: defender_client.antiphishing_policies = { "Default": AntiphishingPolicy( + name="Default", spoof_intelligence=False, spoof_intelligence_action="None", dmarc_reject_action="None", @@ -216,7 +387,7 @@ class Test_defender_antiphishing_policy_configured: default=True, ) } - defender_client.antiphising_rules = {} + defender_client.antiphishing_rules = {} check = defender_antiphishing_policy_configured() result = check.execute() @@ -224,15 +395,103 @@ class Test_defender_antiphishing_policy_configured: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Anti-phishing policy Default is not properly configured." + == "Default is the only policy and it's not properly configured in the default Defender Anti-Phishing Policy." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert ( result[0].resource == defender_client.antiphishing_policies["Default"].dict() ) - assert result[0].resource_name == "Defender Anti-Phishing Policy" + + def test_case_6_both_policies_not_properly_configured(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import ( + defender_antiphishing_policy_configured, + ) + from prowler.providers.m365.services.defender.defender_service import ( + AntiphishingPolicy, + AntiphishingRule, + ) + + defender_client.antiphishing_policies = { + "Default": AntiphishingPolicy( + name="Default", + spoof_intelligence=False, + spoof_intelligence_action="None", + dmarc_reject_action="None", + dmarc_quarantine_action="None", + safety_tips=False, + unauthenticated_sender_action=False, + show_tag=False, + honor_dmarc_policy=False, + default=True, + ), + "Policy1": AntiphishingPolicy( + name="Policy1", + spoof_intelligence=False, + spoof_intelligence_action="None", + dmarc_reject_action="None", + dmarc_quarantine_action="None", + safety_tips=False, + unauthenticated_sender_action=False, + show_tag=False, + honor_dmarc_policy=False, + default=False, + ), + } + defender_client.antiphishing_rules = { + "Policy1": AntiphishingRule( + state="Enabled", + priority=1, + users=[], + groups=[], + domains=["example.com"], + ) + } + + check = defender_antiphishing_policy_configured() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is not properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" assert result[0].resource_id == "Default" - assert result[0].location == "global" + assert ( + result[0].resource + == defender_client.antiphishing_policies["Default"].dict() + ) + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Anti-phishing policy Policy1 is not properly configured and includes domains: example.com, " + "with priority 1 (0 is the highest). Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.antiphishing_policies["Policy1"].dict() + ) def test_no_antiphishing_policies(self): defender_client = mock.MagicMock() @@ -257,64 +516,8 @@ class Test_defender_antiphishing_policy_configured: ) defender_client.antiphishing_policies = {} - defender_client.antiphising_rules = {} + defender_client.antiphishing_rules = {} check = defender_antiphishing_policy_configured() result = check.execute() - assert result == [] - - def test_custom_policy_without_rule(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import ( - defender_antiphishing_policy_configured, - ) - from prowler.providers.m365.services.defender.defender_service import ( - AntiphishingPolicy, - ) - - defender_client.antiphishing_policies = { - "PolicyX": AntiphishingPolicy( - spoof_intelligence=True, - spoof_intelligence_action="Quarantine", - dmarc_reject_action="Quarantine", - dmarc_quarantine_action="Quarantine", - safety_tips=True, - unauthenticated_sender_action=True, - show_tag=True, - honor_dmarc_policy=True, - default=False, - ) - } - defender_client.antiphising_rules = {} - - check = defender_antiphishing_policy_configured() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Anti-phishing policy PolicyX is not properly configured." - ) - assert ( - result[0].resource - == defender_client.antiphishing_policies["PolicyX"].dict() - ) - assert result[0].resource_name == "Defender Anti-Phishing Policy" - assert result[0].resource_id == "PolicyX" - assert result[0].location == "global" + assert len(result) == 0 diff --git a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py index dbb3ee3344..7baedbeb61 100644 --- a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py +++ b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py @@ -4,119 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider class Test_defender_antispam_outbound_policy_configured: - def test_properly_configured_custom_policy(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import ( - defender_antispam_outbound_policy_configured, - ) - from prowler.providers.m365.services.defender.defender_service import ( - OutboundSpamPolicy, - OutboundSpamRule, - ) - - defender_client.outbound_spam_policies = { - "Policy1": OutboundSpamPolicy( - notify_sender_blocked=True, - notify_limit_exceeded=True, - notify_limit_exceeded_addresses=["test@correo.com"], - notify_sender_blocked_addresses=["test@correo.com"], - default=False, - auto_forwarding_mode=False, - ) - } - defender_client.outbound_spam_rules = { - "Policy1": OutboundSpamRule(state="Enabled") - } - - check = defender_antispam_outbound_policy_configured() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - result[0].status_extended - == "Outbound Spam Policy Policy1 is properly configured and enabled." - ) - assert ( - result[0].resource - == defender_client.outbound_spam_policies["Policy1"].dict() - ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "Policy1" - assert result[0].location == "global" - - def test_not_properly_configured_policy(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import ( - defender_antispam_outbound_policy_configured, - ) - from prowler.providers.m365.services.defender.defender_service import ( - OutboundSpamPolicy, - OutboundSpamRule, - ) - - defender_client.outbound_spam_policies = { - "Policy2": OutboundSpamPolicy( - notify_sender_blocked=False, - notify_limit_exceeded=False, - notify_limit_exceeded_addresses=[], - notify_sender_blocked_addresses=[], - default=False, - auto_forwarding_mode=False, - ) - } - defender_client.outbound_spam_rules = { - "Policy2": OutboundSpamRule(state="Enabled") - } - - check = defender_antispam_outbound_policy_configured() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Outbound Spam Policy Policy2 is not properly configured." - ) - assert ( - result[0].resource - == defender_client.outbound_spam_policies["Policy2"].dict() - ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "Policy2" - assert result[0].location == "global" - - def test_properly_configured_default_policy(self): + def test_case_1_default_policy_properly_configured(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -143,12 +31,13 @@ class Test_defender_antispam_outbound_policy_configured: defender_client.outbound_spam_policies = { "Default": OutboundSpamPolicy( - notify_sender_blocked=True, + name="Default", notify_limit_exceeded=True, - notify_limit_exceeded_addresses=["test@correo.com"], - notify_sender_blocked_addresses=["test@correo.com"], - default=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], auto_forwarding_mode=False, + default=True, ) } defender_client.outbound_spam_rules = {} @@ -159,17 +48,271 @@ class Test_defender_antispam_outbound_policy_configured: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Outbound Spam Policy Default is properly configured and enabled." + == "Default is the only policy and it's properly configured in the default Defender Outbound Spam Policy." ) + assert ( + result[0].resource_name + == defender_client.outbound_spam_policies["Default"].name + ) + assert result[0].resource_id == "Default" assert ( result[0].resource == defender_client.outbound_spam_policies["Default"].dict() ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "Default" - assert result[0].location == "global" - def test_policy_without_rule(self): + def test_case_2_all_policies_properly_configured(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import ( + defender_antispam_outbound_policy_configured, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + auto_forwarding_mode=False, + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + auto_forwarding_mode=False, + default=False, + ), + } + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_antispam_outbound_policy_configured() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is properly configured in the default Defender Outbound Spam Policy, but could be overridden by another bad-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 is properly configured and includes users: test@example.com; groups: group1; domains: example.com, " + "with priority 1 (0 is the highest). Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() + ) + + def test_case_3_default_ok_others_not(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import ( + defender_antispam_outbound_policy_configured, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + auto_forwarding_mode=False, + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + auto_forwarding_mode=False, + default=False, + ), + } + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_antispam_outbound_policy_configured() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is properly configured in the default Defender Outbound Spam Policy, but could be overridden by another bad-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 is not properly configured and includes users: test@example.com; groups: group1; domains: example.com, " + "with priority 1 (0 is the highest). However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() + ) + + def test_case_4_default_not_ok_custom_good(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import ( + defender_antispam_outbound_policy_configured, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + auto_forwarding_mode=False, + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + auto_forwarding_mode=False, + default=False, + ), + } + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=0, + users=["user1@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_antispam_outbound_policy_configured() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is not properly configured in the default Defender Outbound Spam Policy, but could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 is properly configured and includes users: user1@example.com; groups: group1; domains: domain.com, " + "with priority 0 (0 is the highest). However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() + ) + + def test_case_5_only_default_not_ok(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -195,13 +338,14 @@ class Test_defender_antispam_outbound_policy_configured: ) defender_client.outbound_spam_policies = { - "PolicyX": OutboundSpamPolicy( - notify_sender_blocked=True, - notify_limit_exceeded=True, - notify_limit_exceeded_addresses=["admin@org.com"], - notify_sender_blocked_addresses=["admin@org.com"], - default=False, + "Default": OutboundSpamPolicy( + name="Default", + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], auto_forwarding_mode=False, + default=True, ) } defender_client.outbound_spam_rules = {} @@ -212,15 +356,98 @@ class Test_defender_antispam_outbound_policy_configured: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Outbound Spam Policy PolicyX is not properly configured." + == "Default is the only policy and it's not properly configured in the default Defender Outbound Spam Policy." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert ( result[0].resource - == defender_client.outbound_spam_policies["PolicyX"].dict() + == defender_client.outbound_spam_policies["Default"].dict() + ) + + def test_case_6_default_and_custom_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import ( + defender_antispam_outbound_policy_configured, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + auto_forwarding_mode=False, + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + auto_forwarding_mode=False, + default=False, + ), + } + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=5, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_antispam_outbound_policy_configured() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is not properly configured in the default Defender Outbound Spam Policy, but could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 is not properly configured and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 5 (0 is the highest). Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "PolicyX" - assert result[0].location == "global" def test_no_outbound_spam_policies(self): defender_client = mock.MagicMock() diff --git a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py index 0e911a33b2..64fce5f88b 100644 --- a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py +++ b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py @@ -4,6 +4,456 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider class Test_defender_antispam_outbound_policy_forwarding_disabled: + def test_case_1_default_policy_forwarding_disabled(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( + defender_antispam_outbound_policy_forwarding_disabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + auto_forwarding_mode=False, + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + default=True, + ) + } + defender_client.outbound_spam_rules = {} + + check = defender_antispam_outbound_policy_forwarding_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the only policy and mail forwarding is disabled." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + def test_case_2_all_policies_forwarding_disabled(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( + defender_antispam_outbound_policy_forwarding_disabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + auto_forwarding_mode=False, + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + auto_forwarding_mode=False, + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + default=False, + ), + } + + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_antispam_outbound_policy_forwarding_disabled() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and mail forwarding is disabled, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 disables mail forwarding and includes users: test@example.com; groups: group1; domains: example.com, " + "with priority 1 (0 is the highest). Also, the default policy disables mail forwarding, so entities not included by this custom policy could still be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() + ) + + def test_case_3_default_ok_custom_not(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( + defender_antispam_outbound_policy_forwarding_disabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + auto_forwarding_mode=False, + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + auto_forwarding_mode=True, + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + default=False, + ), + } + + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_antispam_outbound_policy_forwarding_disabled() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and mail forwarding is disabled, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 allows mail forwarding and includes users: test@example.com; groups: group1; domains: example.com, " + "with priority 1 (0 is the highest). However, the default policy disables mail forwarding, so entities not included by this custom policy could be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() + ) + + def test_case_4_default_not_ok_custom_good(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( + defender_antispam_outbound_policy_forwarding_disabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + auto_forwarding_mode=True, + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + auto_forwarding_mode=False, + notify_limit_exceeded=True, + notify_sender_blocked=True, + notify_limit_exceeded_addresses=["admin@example.com"], + notify_sender_blocked_addresses=["admin@example.com"], + default=False, + ), + } + + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=0, + users=["user1@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_antispam_outbound_policy_forwarding_disabled() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and mail forwarding is allowed, but it could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 disables mail forwarding and includes users: user1@example.com; groups: group1; domains: domain.com, " + "with priority 0 (0 is the highest). However, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() + ) + + def test_case_5_only_default_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( + defender_antispam_outbound_policy_forwarding_disabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + auto_forwarding_mode=True, + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + default=True, + ) + } + defender_client.outbound_spam_rules = {} + + check = defender_antispam_outbound_policy_forwarding_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the only policy and mail forwarding is allowed." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + def test_case_6_default_and_custom_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( + defender_antispam_outbound_policy_forwarding_disabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + OutboundSpamPolicy, + OutboundSpamRule, + ) + + defender_client.outbound_spam_policies = { + "Default": OutboundSpamPolicy( + name="Default", + auto_forwarding_mode=True, + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + default=True, + ), + "Policy1": OutboundSpamPolicy( + name="Policy1", + auto_forwarding_mode=True, + notify_limit_exceeded=False, + notify_sender_blocked=False, + notify_limit_exceeded_addresses=[], + notify_sender_blocked_addresses=[], + default=False, + ), + } + + defender_client.outbound_spam_rules = { + "Policy1": OutboundSpamRule( + state="Enabled", + priority=1, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_antispam_outbound_policy_forwarding_disabled() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and mail forwarding is allowed, but it could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert ( + result[0].resource + == defender_client.outbound_spam_policies["Default"].dict() + ) + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Outbound Spam policy Policy1 allows mail forwarding and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 1 (0 is the highest). Also, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert ( + result[1].resource + == defender_client.outbound_spam_policies["Policy1"].dict() + ) + def test_no_outbound_spam_policies(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" @@ -32,221 +482,3 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: check = defender_antispam_outbound_policy_forwarding_disabled() result = check.execute() assert len(result) == 0 - - def test_forwarding_disabled_custom_policy(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( - defender_antispam_outbound_policy_forwarding_disabled, - ) - from prowler.providers.m365.services.defender.defender_service import ( - OutboundSpamPolicy, - OutboundSpamRule, - ) - - defender_client.outbound_spam_policies = { - "Policy1": OutboundSpamPolicy( - default=False, - notify_sender_blocked=True, - notify_limit_exceeded=True, - notify_limit_exceeded_addresses=["test@correo.com"], - notify_sender_blocked_addresses=["test@correo.com"], - auto_forwarding_mode=False, - ) - } - defender_client.outbound_spam_rules = { - "Policy1": OutboundSpamRule(state="Enabled") - } - - check = defender_antispam_outbound_policy_forwarding_disabled() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - result[0].status_extended - == "Outbound Spam Policy Policy1 does not allow mail forwarding." - ) - assert ( - result[0].resource - == defender_client.outbound_spam_policies["Policy1"].dict() - ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "Policy1" - assert result[0].location == "global" - - def test_forwarding_enabled_policy(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( - defender_antispam_outbound_policy_forwarding_disabled, - ) - from prowler.providers.m365.services.defender.defender_service import ( - OutboundSpamPolicy, - OutboundSpamRule, - ) - - defender_client.outbound_spam_policies = { - "Policy2": OutboundSpamPolicy( - default=False, - notify_sender_blocked=True, - notify_limit_exceeded=True, - notify_limit_exceeded_addresses=["test@correo.com"], - notify_sender_blocked_addresses=["test@correo.com"], - auto_forwarding_mode=True, - ) - } - defender_client.outbound_spam_rules = { - "Policy2": OutboundSpamRule(state="Enabled") - } - - check = defender_antispam_outbound_policy_forwarding_disabled() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Outbound Spam Policy Policy2 does allow mail forwarding." - ) - assert ( - result[0].resource - == defender_client.outbound_spam_policies["Policy2"].dict() - ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "Policy2" - assert result[0].location == "global" - - def test_forwarding_disabled_default_policy(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( - defender_antispam_outbound_policy_forwarding_disabled, - ) - from prowler.providers.m365.services.defender.defender_service import ( - OutboundSpamPolicy, - ) - - defender_client.outbound_spam_policies = { - "Default": OutboundSpamPolicy( - default=True, - notify_sender_blocked=True, - notify_limit_exceeded=True, - notify_limit_exceeded_addresses=["test@correo.com"], - notify_sender_blocked_addresses=["test@correo.com"], - auto_forwarding_mode=False, - ) - } - defender_client.outbound_spam_rules = {} - - check = defender_antispam_outbound_policy_forwarding_disabled() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - result[0].status_extended - == "Outbound Spam Policy Default does not allow mail forwarding." - ) - assert ( - result[0].resource - == defender_client.outbound_spam_policies["Default"].dict() - ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "Default" - assert result[0].location == "global" - - def test_policy_without_rule(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import ( - defender_antispam_outbound_policy_forwarding_disabled, - ) - from prowler.providers.m365.services.defender.defender_service import ( - OutboundSpamPolicy, - ) - - defender_client.outbound_spam_policies = { - "PolicyX": OutboundSpamPolicy( - default=False, - notify_sender_blocked=True, - notify_limit_exceeded=True, - notify_limit_exceeded_addresses=["test@correo.com"], - notify_sender_blocked_addresses=["test@correo.com"], - auto_forwarding_mode=False, - ) - } - defender_client.outbound_spam_rules = {} - - check = defender_antispam_outbound_policy_forwarding_disabled() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Outbound Spam Policy PolicyX does allow mail forwarding." - ) - assert ( - result[0].resource - == defender_client.outbound_spam_policies["PolicyX"].dict() - ) - assert result[0].resource_name == "Defender Outbound Spam Policy" - assert result[0].resource_id == "PolicyX" - assert result[0].location == "global" diff --git a/tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py b/tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py index feba0c5bb4..625fc8ded3 100644 --- a/tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py +++ b/tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py @@ -4,7 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider class Test_defender_antispam_policy_inbound_no_allowed_domains: - def test_policy_without_allowed_domains(self): + def test_case_1_default_policy_no_allowed_domains(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -31,26 +31,240 @@ class Test_defender_antispam_policy_inbound_no_allowed_domains: defender_client.inbound_spam_policies = [ DefenderInboundSpamPolicy( - identity="Policy1", + identity="Default", + default=True, allowed_sender_domains=[], ) ] + defender_client.inbound_spam_rules = {} check = defender_antispam_policy_inbound_no_allowed_domains() result = check.execute() - assert len(result) == 1 assert result[0].status == "PASS" assert ( result[0].status_extended - == "Inbound anti-spam policy Policy1 does not contain allowed domains." + == "Default is the only policy and it does not contain allowed domains." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert result[0].resource == defender_client.inbound_spam_policies[0].dict() - assert result[0].resource_name == "Defender Inbound Spam Policy" - assert result[0].resource_id == "Policy1" - assert result[0].location == "global" - def test_policy_with_allowed_domains(self): + def test_case_2_all_policies_no_allowed_domains(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import ( + defender_antispam_policy_inbound_no_allowed_domains, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DefenderInboundSpamPolicy, + InboundSpamRule, + ) + + defender_client.inbound_spam_policies = [ + DefenderInboundSpamPolicy( + identity="Default", + default=True, + allowed_sender_domains=[], + ), + DefenderInboundSpamPolicy( + identity="Policy1", + default=False, + allowed_sender_domains=[], + ), + ] + defender_client.inbound_spam_rules = { + "Policy1": InboundSpamRule( + state="Enabled", + priority=1, + users=["user1@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_antispam_policy_inbound_no_allowed_domains() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and it does not contain allowed domains, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.inbound_spam_policies[0].dict() + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Inbound Spam policy Policy1 does not contain allowed domains and includes users: user1@example.com; groups: group1; domains: example.com, " + "with priority 1 (0 is the highest). Also, the default policy does not contain allowed domains, so entities not included by this custom policy could still be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.inbound_spam_policies[1].dict() + + def test_case_3_default_ok_custom_with_allowed_domains(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import ( + defender_antispam_policy_inbound_no_allowed_domains, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DefenderInboundSpamPolicy, + InboundSpamRule, + ) + + defender_client.inbound_spam_policies = [ + DefenderInboundSpamPolicy( + identity="Default", + default=True, + allowed_sender_domains=[], + ), + DefenderInboundSpamPolicy( + identity="Policy1", + default=False, + allowed_sender_domains=["spam.com"], + ), + ] + defender_client.inbound_spam_rules = { + "Policy1": InboundSpamRule( + state="Enabled", + priority=2, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_antispam_policy_inbound_no_allowed_domains() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and it does not contain allowed domains, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.inbound_spam_policies[0].dict() + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Inbound Spam policy Policy1 contains allowed domains and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 2 (0 is the highest). However, the default policy does not contain allowed domains, so entities not included by this custom policy could be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.inbound_spam_policies[1].dict() + + def test_case_4_default_with_allowed_domains_custom_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import ( + defender_antispam_policy_inbound_no_allowed_domains, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DefenderInboundSpamPolicy, + InboundSpamRule, + ) + + defender_client.inbound_spam_policies = [ + DefenderInboundSpamPolicy( + identity="Default", + default=True, + allowed_sender_domains=["example.org"], + ), + DefenderInboundSpamPolicy( + identity="Policy1", + default=False, + allowed_sender_domains=[], + ), + ] + + defender_client.inbound_spam_rules = { + "Policy1": InboundSpamRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_antispam_policy_inbound_no_allowed_domains() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and it contains allowed domains: example.org, but it could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.inbound_spam_policies[0].dict() + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Inbound Spam policy Policy1 does not contain allowed domains and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 0 (0 is the highest). However, the default policy contains allowed domains, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.inbound_spam_policies[1].dict() + + def test_case_5_default_with_allowed_domains(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -77,24 +291,96 @@ class Test_defender_antispam_policy_inbound_no_allowed_domains: defender_client.inbound_spam_policies = [ DefenderInboundSpamPolicy( - identity="Policy2", - allowed_sender_domains=["bad-domain.com"], + identity="Default", + default=True, + allowed_sender_domains=["example.com"], ) ] + defender_client.inbound_spam_rules = {} check = defender_antispam_policy_inbound_no_allowed_domains() result = check.execute() - assert len(result) == 1 assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Inbound anti-spam policy Policy2 contains allowed domains: ['bad-domain.com']." + == "Default is the only policy and it contains allowed domains: example.com." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert result[0].resource == defender_client.inbound_spam_policies[0].dict() - assert result[0].resource_name == "Defender Inbound Spam Policy" - assert result[0].resource_id == "Policy2" - assert result[0].location == "global" + + def test_case_6_both_default_and_custom_with_allowed_domains(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import ( + defender_antispam_policy_inbound_no_allowed_domains, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DefenderInboundSpamPolicy, + InboundSpamRule, + ) + + defender_client.inbound_spam_policies = [ + DefenderInboundSpamPolicy( + identity="Default", + default=True, + allowed_sender_domains=["example.com"], + ), + DefenderInboundSpamPolicy( + identity="Policy1", + default=False, + allowed_sender_domains=["spam.com"], + ), + ] + + defender_client.inbound_spam_rules = { + "Policy1": InboundSpamRule( + state="Enabled", + priority=5, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_antispam_policy_inbound_no_allowed_domains() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and it contains allowed domains: example.com, but it could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.inbound_spam_policies[0].dict() + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Inbound Spam policy Policy1 contains allowed domains and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 5 (0 is the highest). Also, the default policy contains allowed domains, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.inbound_spam_policies[1].dict() def test_no_inbound_spam_policies(self): defender_client = mock.MagicMock() @@ -119,8 +405,8 @@ class Test_defender_antispam_policy_inbound_no_allowed_domains: ) defender_client.inbound_spam_policies = [] + defender_client.inbound_spam_rules = {} check = defender_antispam_policy_inbound_no_allowed_domains() result = check.execute() - assert len(result) == 0 diff --git a/tests/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled_test.py b/tests/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled_test.py index d7cbb59d9e..adb62b213d 100644 --- a/tests/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled_test.py +++ b/tests/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled_test.py @@ -4,7 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider class Test_defender_malware_policy_common_attachments_filter_enabled: - def test_enable_file_filter_disabled(self): + def test_case_1_only_default_policy_enabled(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -31,30 +31,29 @@ class Test_defender_malware_policy_common_attachments_filter_enabled: defender_client.malware_policies = [ MalwarePolicy( - enable_file_filter=False, - identity="Policy1", - enable_internal_sender_admin_notifications=False, - internal_sender_admin_address="", - file_types=[], + identity="Default", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe", "js"], is_default=True, - ), + ) ] + defender_client.malware_rules = {} check = defender_malware_policy_common_attachments_filter_enabled() result = check.execute() assert len(result) == 1 - - assert result[0].status == "FAIL" + assert result[0].status == "PASS" assert ( result[0].status_extended - == "Common Attachment Types Filter is not enabled in anti-malware policy Policy1." + == "Default is the only policy and Common Attachment Types Filter is enabled." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - def test_enable_file_filter_enabled(self): + def test_case_2_all_policies_enabled(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -77,34 +76,345 @@ class Test_defender_malware_policy_common_attachments_filter_enabled: ) from prowler.providers.m365.services.defender.defender_service import ( MalwarePolicy, + MalwareRule, ) defender_client.malware_policies = [ MalwarePolicy( + identity="Default", enable_file_filter=True, - identity="Policy1", - enable_internal_sender_admin_notifications=False, - internal_sender_admin_address="", - file_types=[], + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe", "js"], is_default=True, ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe"], + is_default=False, + ), ] + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=1, + users=["user1@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + check = defender_malware_policy_common_attachments_filter_enabled() result = check.execute() - assert len(result) == 1 + assert len(result) == 2 assert result[0].status == "PASS" assert ( result[0].status_extended - == "Common Attachment Types Filter is enabled in anti-malware policy Policy1." + == "Default is the default policy and Common Attachment Types Filter is enabled, but it could be overridden by another misconfigured Custom Policy." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - def test_no_policy(self): + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 enables Common Attachment Types Filter and includes users: user1@example.com; groups: group1; domains: domain.com, " + "with priority 1 (0 is the highest). Also, the default policy enables the filter, so entities not included by this custom policy could still be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_3_default_ok_custom_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import ( + defender_malware_policy_common_attachments_filter_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe"], + is_default=True, + ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["js"], + is_default=False, + ), + ] + + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=2, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_malware_policy_common_attachments_filter_enabled() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and Common Attachment Types Filter is enabled, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 does not enable Common Attachment Types Filter and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 2 (0 is the highest). However, the default policy enables the filter, so entities not included by this custom policy could be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_4_default_not_ok_custom_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import ( + defender_malware_policy_common_attachments_filter_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe"], + is_default=False, + ), + ] + + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_malware_policy_common_attachments_filter_enabled() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and Common Attachment Types Filter is not enabled, but it could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 enables Common Attachment Types Filter and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 0 (0 is the highest). However, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_5_only_default_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import ( + defender_malware_policy_common_attachments_filter_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ) + ] + defender_client.malware_rules = {} + + check = defender_malware_policy_common_attachments_filter_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the only policy and Common Attachment Types Filter is not enabled." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + def test_case_6_default_and_custom_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import ( + defender_malware_policy_common_attachments_filter_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe"], + is_default=False, + ), + ] + + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=5, + users=["user1@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_malware_policy_common_attachments_filter_enabled() + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and Common Attachment Types Filter is not enabled, but it could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 does not enable Common Attachment Types Filter and includes users: user1@example.com; groups: group1; domains: domain.com, " + "with priority 5 (0 is the highest). Also, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_no_malware_policies(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -127,16 +437,8 @@ class Test_defender_malware_policy_common_attachments_filter_enabled: ) defender_client.malware_policies = [] + defender_client.malware_rules = {} check = defender_malware_policy_common_attachments_filter_enabled() result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Common Attachment Types Filter is not enabled." - ) - assert result[0].resource == {} - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" + assert len(result) == 0 diff --git a/tests/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied_test.py b/tests/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied_test.py index 9977de9a54..682f86c768 100644 --- a/tests/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied_test.py +++ b/tests/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied_test.py @@ -1,53 +1,10 @@ from unittest import mock -from prowler.providers.m365.services.defender.defender_service import ( - MalwarePolicy, - MalwareRule, -) from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider class Test_defender_malware_policy_comprehensive_attachments_filter_applied: - def test_no_policy(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_client.audited_domain = DOMAIN - defender_client.malware_policies = [] - defender_client.audit_config = {} - defender_client.malware_rules = {} - - 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_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( - defender_malware_policy_comprehensive_attachments_filter_applied, - ) - - check = defender_malware_policy_comprehensive_attachments_filter_applied() - result = check.execute() - - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Common Attachment Types Filter is not enabled." - ) - assert result[0].resource == {} - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - - def test_policy_enabled_all_extensions_blocked(self): + def test_case_1_default_policy_properly_configured(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -68,192 +25,430 @@ class Test_defender_malware_policy_comprehensive_attachments_filter_applied: from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( defender_malware_policy_comprehensive_attachments_filter_applied, ) - - valid_extensions = ["exe", "bat", "js"] - defender_client.audit_config = { - "recommended_blocked_file_types": valid_extensions - } - defender_client.malware_rules = {"PolicyGood": MalwareRule(state="Enabled")} - defender_client.malware_policies = [ - MalwarePolicy( - enable_file_filter=True, - identity="PolicyGood", - enable_internal_sender_admin_notifications=True, - internal_sender_admin_address="admin@example.com", - file_types=valid_extensions, - is_default=True, - ) - ] - - check = defender_malware_policy_comprehensive_attachments_filter_applied() - result = check.execute() - - assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - result[0].status_extended - == "Common Attachment Types Filter is enabled in anti-malware policy PolicyGood, the policy is enabled and the filter is applied to the recommended file types." - ) - assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - - def test_policy_enabled_missing_extensions(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( - defender_malware_policy_comprehensive_attachments_filter_applied, + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, ) defender_client.audit_config = { "recommended_blocked_file_types": ["exe", "bat", "js"] } - defender_client.malware_rules = { - "PolicyPartial": MalwareRule(state="Enabled") - } defender_client.malware_policies = [ MalwarePolicy( + identity="Default", enable_file_filter=True, - identity="PolicyPartial", enable_internal_sender_admin_notifications=True, internal_sender_admin_address="admin@example.com", - file_types=["exe"], + file_types=["exe", "bat", "js"], is_default=True, ) ] - - check = defender_malware_policy_comprehensive_attachments_filter_applied() - result = check.execute() - - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Common Attachment Types Filter is enabled in anti-malware policy PolicyPartial, but the following recommended file types are missing: bat, js." - ) - assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - - def test_policy_enabled_but_rule_disabled(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( - defender_malware_policy_comprehensive_attachments_filter_applied, - ) - - valid_extensions = ["exe"] - defender_client.audit_config = { - "recommended_blocked_file_types": valid_extensions - } - defender_client.malware_rules = { - "PolicyDisabled": MalwareRule(state="Disabled") - } - defender_client.malware_policies = [ - MalwarePolicy( - enable_file_filter=True, - identity="PolicyDisabled", - enable_internal_sender_admin_notifications=True, - internal_sender_admin_address="admin@example.com", - file_types=valid_extensions, - is_default=False, - ) - ] - - check = defender_malware_policy_comprehensive_attachments_filter_applied() - result = check.execute() - - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == "Common Attachment Types Filter is enabled in anti-malware policy PolicyDisabled, but the policy is disabled." - ) - assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - - def test_policy_enabled_but_no_rule_and_not_default(self): - defender_client = mock.MagicMock() - defender_client.audited_tenant = "audited_tenant" - defender_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_exchange_online" - ), - mock.patch( - "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", - new=defender_client, - ), - ): - from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( - defender_malware_policy_comprehensive_attachments_filter_applied, - ) - - valid_extensions = ["exe"] - defender_client.audit_config = { - "recommended_blocked_file_types": valid_extensions - } defender_client.malware_rules = {} - defender_client.malware_policies = [ - MalwarePolicy( - enable_file_filter=True, - identity="PolicyNoRule", - enable_internal_sender_admin_notifications=True, - internal_sender_admin_address="admin@example.com", - file_types=valid_extensions, - is_default=False, - ) - ] check = defender_malware_policy_comprehensive_attachments_filter_applied() result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the only policy and Common Attachment Types Filter is properly configured." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + def test_case_2_all_policies_properly_configured(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( + defender_malware_policy_comprehensive_attachments_filter_applied, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.audit_config = { + "recommended_blocked_file_types": ["exe", "bat"] + } + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe", "bat"], + is_default=True, + ), + MalwarePolicy( + identity="Custom1", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe", "bat"], + is_default=False, + ), + ] + defender_client.malware_rules = { + "Custom1": MalwareRule( + state="Enabled", + priority=1, + users=["user1@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_malware_policy_comprehensive_attachments_filter_applied() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and Common Attachment Types Filter is properly configured, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Malware policy Custom1 is properly configured and includes users: user1@example.com; groups: group1; domains: example.com, " + "with priority 1 (0 is the highest). Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + assert result[1].resource_name == "Custom1" + assert result[1].resource_id == "Custom1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_3_default_ok_custom_not(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( + defender_malware_policy_comprehensive_attachments_filter_applied, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.audit_config = { + "recommended_blocked_file_types": ["exe", "bat"] + } + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe", "bat"], + is_default=True, + ), + MalwarePolicy( + identity="Custom1", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe"], # missing bat + is_default=False, + ), + ] + defender_client.malware_rules = { + "Custom1": MalwareRule( + state="Enabled", + priority=1, + users=["user1@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_malware_policy_comprehensive_attachments_filter_applied() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and Common Attachment Types Filter is properly configured, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Malware policy Custom1 is not properly configured and includes users: user1@example.com; groups: group1; domains: example.com, " + "with priority 1 (0 is the highest). Missing recommended file types: bat. However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + assert result[1].resource_name == "Custom1" + assert result[1].resource_id == "Custom1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_4_default_not_ok_custom_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( + defender_malware_policy_comprehensive_attachments_filter_applied, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.audit_config = { + "recommended_blocked_file_types": ["exe", "bat"] + } + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ), + MalwarePolicy( + identity="Custom1", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe", "bat"], + is_default=False, + ), + ] + defender_client.malware_rules = { + "Custom1": MalwareRule( + state="Enabled", + priority=0, + users=["user1@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = defender_malware_policy_comprehensive_attachments_filter_applied() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and Common Attachment Types Filter is not properly configured, but it could be overridden by another well-configured Custom Policy. Missing recommended file types: exe, bat." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Malware policy Custom1 is properly configured and includes users: user1@example.com; groups: group1; domains: domain.com, " + "with priority 0 (0 is the highest). However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Custom1" + assert result[1].resource_id == "Custom1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_5_only_default_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( + defender_malware_policy_comprehensive_attachments_filter_applied, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + ) + + defender_client.audit_config = { + "recommended_blocked_file_types": ["exe", "bat"] + } + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ) + ] + defender_client.malware_rules = {} + + check = defender_malware_policy_comprehensive_attachments_filter_applied() + result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Common Attachment Types Filter is not properly configured in anti-malware policy PolicyNoRule." + == "Default is the only policy and Common Attachment Types Filter is not properly configured. Missing recommended file types: exe, bat." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" + + def test_case_6_default_and_custom_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( + defender_malware_policy_comprehensive_attachments_filter_applied, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.audit_config = { + "recommended_blocked_file_types": ["exe", "bat"] + } + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ), + MalwarePolicy( + identity="Custom1", + enable_file_filter=True, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=["exe"], # missing bat + is_default=False, + ), + ] + defender_client.malware_rules = { + "Custom1": MalwareRule( + state="Enabled", + priority=2, + users=["user@example.com"], + groups=["group1"], + domains=["example.com"], + ) + } + + check = defender_malware_policy_comprehensive_attachments_filter_applied() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and Common Attachment Types Filter is not properly configured, but it could be overridden by another well-configured Custom Policy. Missing recommended file types: exe, bat." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Malware policy Custom1 is not properly configured and includes users: user@example.com; groups: group1; domains: example.com, " + "with priority 2 (0 is the highest). Missing recommended file types: bat. Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Custom1" + assert result[1].resource_id == "Custom1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_no_malware_policies(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.malware_policies = [] + defender_client.malware_rules = {} + defender_client.audit_config = {} + + 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_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import ( + defender_malware_policy_comprehensive_attachments_filter_applied, + ) + + check = defender_malware_policy_comprehensive_attachments_filter_applied() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled_test.py b/tests/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled_test.py index fdd26415d0..16fe656c2b 100644 --- a/tests/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled_test.py +++ b/tests/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled_test.py @@ -4,7 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider class Test_defender_malware_policy_notifications_internal_users_malware_enabled: - def test_notifications_disabled(self): + def test_case_1_default_properly_configured(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -31,32 +31,322 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled: defender_client.malware_policies = [ MalwarePolicy( - enable_file_filter=True, identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ) + ] + defender_client.malware_rules = {} + + check = ( + defender_malware_policy_notifications_internal_users_malware_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the only policy and notifications for internal users sending malware are enabled." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + def test_case_2_default_and_custom_properly_configured(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import ( + defender_malware_policy_notifications_internal_users_malware_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=False, + ), + ] + + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=1, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = ( + defender_malware_policy_notifications_internal_users_malware_enabled() + ) + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and notifications for internal users sending malware are enabled, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 is properly configured and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 1 (0 is the highest). Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_3_default_ok_custom_not_configured(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import ( + defender_malware_policy_notifications_internal_users_malware_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=True, + ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=False, + enable_internal_sender_admin_notifications=False, + internal_sender_admin_address="", + file_types=[], + is_default=False, + ), + ] + + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=2, + users=["internal@example.com"], + groups=["group2"], + domains=["example.org"], + ) + } + + check = ( + defender_malware_policy_notifications_internal_users_malware_enabled() + ) + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Default is the default policy and notifications for internal users sending malware are enabled, but it could be overridden by another misconfigured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 is not properly configured and includes users: internal@example.com; groups: group2; domains: example.org, " + "with priority 2 (0 is the highest). However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_4_default_not_ok_custom_properly_configured(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import ( + defender_malware_policy_notifications_internal_users_malware_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + MalwareRule, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, + enable_internal_sender_admin_notifications=False, + internal_sender_admin_address="", + file_types=[], + is_default=True, + ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=False, + enable_internal_sender_admin_notifications=True, + internal_sender_admin_address="admin@example.com", + file_types=[], + is_default=False, + ), + ] + + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=["group1"], + domains=["domain.com"], + ) + } + + check = ( + defender_malware_policy_notifications_internal_users_malware_enabled() + ) + result = check.execute() + assert len(result) == 2 + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Default is the default policy and notifications for internal users sending malware are not enabled, but it could be overridden by another well-configured Custom Policy." + ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" + assert result[0].resource == defender_client.malware_policies[0].dict() + + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 is properly configured and includes users: user@example.com; groups: group1; domains: domain.com, " + "with priority 0 (0 is the highest). However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_case_5_only_default_not_ok(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import ( + defender_malware_policy_notifications_internal_users_malware_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + MalwarePolicy, + ) + + defender_client.malware_policies = [ + MalwarePolicy( + identity="Default", + enable_file_filter=False, enable_internal_sender_admin_notifications=False, internal_sender_admin_address="", file_types=[], is_default=True, ) ] + defender_client.malware_rules = {} check = ( defender_malware_policy_notifications_internal_users_malware_enabled() ) result = check.execute() - assert len(result) == 1 assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Notifications for internal users sending malware are not enabled." + == "Default is the only policy and notifications for internal users sending malware are not enabled." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - def test_notifications_enabled_without_email(self): + def test_case_6_default_and_custom_not_configured(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -79,36 +369,64 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled: ) from prowler.providers.m365.services.defender.defender_service import ( MalwarePolicy, + MalwareRule, ) defender_client.malware_policies = [ MalwarePolicy( - enable_file_filter=True, identity="Default", - enable_internal_sender_admin_notifications=True, + enable_file_filter=False, + enable_internal_sender_admin_notifications=False, internal_sender_admin_address="", file_types=[], is_default=True, - ) + ), + MalwarePolicy( + identity="Policy1", + enable_file_filter=False, + enable_internal_sender_admin_notifications=False, + internal_sender_admin_address="", + file_types=[], + is_default=False, + ), ] + defender_client.malware_rules = { + "Policy1": MalwareRule( + state="Enabled", + priority=3, + users=["u@example.com"], + groups=["g1"], + domains=["d.com"], + ) + } + check = ( defender_malware_policy_notifications_internal_users_malware_enabled() ) result = check.execute() + assert len(result) == 2 - assert len(result) == 1 assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Notifications for internal users sending malware are enabled, but no email addresses are configured." + == "Default is the default policy and notifications for internal users sending malware are not enabled, but it could be overridden by another well-configured Custom Policy." ) + assert result[0].resource_name == "Default" + assert result[0].resource_id == "Default" assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" - def test_notifications_enabled_with_email(self): + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Custom Malware policy Policy1 is not properly configured and includes users: u@example.com; groups: g1; domains: d.com, " + "with priority 3 (0 is the highest). Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected." + ) + assert result[1].resource_name == "Policy1" + assert result[1].resource_id == "Policy1" + assert result[1].resource == defender_client.malware_policies[1].dict() + + def test_no_malware_policies(self): defender_client = mock.MagicMock() defender_client.audited_tenant = "audited_tenant" defender_client.audited_domain = DOMAIN @@ -129,33 +447,12 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled: from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import ( defender_malware_policy_notifications_internal_users_malware_enabled, ) - from prowler.providers.m365.services.defender.defender_service import ( - MalwarePolicy, - ) - defender_client.malware_policies = [ - MalwarePolicy( - enable_file_filter=True, - identity="Default", - enable_internal_sender_admin_notifications=True, - internal_sender_admin_address="security@example.com", - file_types=[], - is_default=True, - ) - ] + defender_client.malware_policies = [] + defender_client.malware_rules = {} check = ( defender_malware_policy_notifications_internal_users_malware_enabled() ) result = check.execute() - - assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - result[0].status_extended - == "Notifications for internal users sending malware are enabled." - ) - assert result[0].resource == defender_client.malware_policies[0].dict() - assert result[0].resource_name == "Defender Malware Policy" - assert result[0].resource_id == "defenderMalwarePolicy" - assert result[0].location == "global" + assert len(result) == 0 diff --git a/tests/providers/m365/services/defender/m365_defender_service_test.py b/tests/providers/m365/services/defender/m365_defender_service_test.py index 3e5a534f2c..35534a53e3 100644 --- a/tests/providers/m365/services/defender/m365_defender_service_test.py +++ b/tests/providers/m365/services/defender/m365_defender_service_test.py @@ -9,6 +9,7 @@ from prowler.providers.m365.services.defender.defender_service import ( Defender, DefenderInboundSpamPolicy, DkimConfig, + InboundSpamRule, MalwarePolicy, MalwareRule, OutboundSpamPolicy, @@ -41,14 +42,27 @@ def mock_defender_get_malware_filter_policy(_): def mock_defender_get_malware_filter_rule(_): return { - "Policy1": MalwareRule(state="Enabled"), - "Policy2": MalwareRule(state="Disabled"), + "Policy1": MalwareRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], + ), + "Policy2": MalwareRule( + state="Disabled", + priority=2, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], + ), } -def mock_defender_get_antiphising_policy(_): +def mock_defender_get_antiphishing_policy(_): return { "Policy1": AntiphishingPolicy( + name="Policy1", spoof_intelligence=True, spoof_intelligence_action="Quarantine", dmarc_reject_action="Reject", @@ -60,6 +74,7 @@ def mock_defender_get_antiphising_policy(_): default=False, ), "Policy2": AntiphishingPolicy( + name="Policy2", spoof_intelligence=False, spoof_intelligence_action="None", dmarc_reject_action="None", @@ -73,13 +88,21 @@ def mock_defender_get_antiphising_policy(_): } -def mock_defender_get_antiphising_rules(_): +def mock_defender_get_antiphishing_rules(_): return { "Policy1": AntiphishingRule( state="Enabled", + priority=1, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], ), "Policy2": AntiphishingRule( state="Disabled", + priority=2, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], ), } @@ -89,14 +112,35 @@ def mock_defender_get_inbound_spam_policy(_): DefenderInboundSpamPolicy( identity="Policy1", allowed_sender_domains=[], + default=False, ), DefenderInboundSpamPolicy( identity="Policy2", allowed_sender_domains=["example.com"], + default=True, ), ] +def mock_defender_get_inbound_spam_rule(_): + return { + "Policy1": InboundSpamRule( + state="Enabled", + priority=1, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], + ), + "Policy2": InboundSpamRule( + state="Disabled", + priority=2, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], + ), + } + + def mock_defender_get_connection_filter_policy(_): return ConnectionFilterPolicy( ip_allow_list=[], @@ -131,6 +175,7 @@ def mock_defender_get_report_submission_policy(_): def mock_defender_get_outbound_spam_filter_policy(_): return { "Policy1": OutboundSpamPolicy( + name="Policy1", notify_sender_blocked=True, notify_limit_exceeded=True, notify_limit_exceeded_addresses=["security@example.com"], @@ -139,6 +184,7 @@ def mock_defender_get_outbound_spam_filter_policy(_): default=False, ), "Policy2": OutboundSpamPolicy( + name="Policy2", notify_sender_blocked=False, notify_limit_exceeded=False, notify_limit_exceeded_addresses=[], @@ -153,9 +199,17 @@ def mock_defender_get_outbound_spam_filter_rule(_): return { "Policy1": OutboundSpamRule( state="Enabled", + priority=1, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], ), "Policy2": OutboundSpamRule( state="Disabled", + priority=2, + users=["test@example.com"], + groups=["example_group"], + domains=["example.com"], ), } @@ -230,14 +284,22 @@ class Test_Defender_Service: ) malware_rules = defender_client.malware_rules assert malware_rules["Policy1"].state == "Enabled" + assert malware_rules["Policy1"].priority == 1 + assert malware_rules["Policy1"].users == ["test@example.com"] + assert malware_rules["Policy1"].groups == ["example_group"] + assert malware_rules["Policy1"].domains == ["example.com"] assert malware_rules["Policy2"].state == "Disabled" + assert malware_rules["Policy2"].priority == 2 + assert malware_rules["Policy2"].users == ["test@example.com"] + assert malware_rules["Policy2"].groups == ["example_group"] + assert malware_rules["Policy2"].domains == ["example.com"] defender_client.powershell.close() @patch( - "prowler.providers.m365.services.defender.defender_service.Defender._get_antiphising_policy", - new=mock_defender_get_antiphising_policy, + "prowler.providers.m365.services.defender.defender_service.Defender._get_antiphishing_policy", + new=mock_defender_get_antiphishing_policy, ) - def test_get_antiphising_policy(self): + def test_get_antiphishing_policy(self): with ( mock.patch( "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" @@ -249,6 +311,7 @@ class Test_Defender_Service: ) ) antiphishing_policies = defender_client.antiphishing_policies + assert antiphishing_policies["Policy1"].name == "Policy1" assert antiphishing_policies["Policy1"].spoof_intelligence is True assert ( antiphishing_policies["Policy1"].spoof_intelligence_action @@ -265,6 +328,7 @@ class Test_Defender_Service: assert antiphishing_policies["Policy1"].show_tag is True assert antiphishing_policies["Policy1"].honor_dmarc_policy is True assert antiphishing_policies["Policy1"].default is False + assert antiphishing_policies["Policy2"].name == "Policy2" assert antiphishing_policies["Policy2"].spoof_intelligence is False assert antiphishing_policies["Policy2"].spoof_intelligence_action == "None" assert antiphishing_policies["Policy2"].dmarc_reject_action == "None" @@ -279,10 +343,10 @@ class Test_Defender_Service: defender_client.powershell.close() @patch( - "prowler.providers.m365.services.defender.defender_service.Defender._get_antiphising_rules", - new=mock_defender_get_antiphising_rules, + "prowler.providers.m365.services.defender.defender_service.Defender._get_antiphishing_rules", + new=mock_defender_get_antiphishing_rules, ) - def test_get_antiphising_rules(self): + def test_get_antiphishing_rules(self): with ( mock.patch( "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" @@ -293,9 +357,17 @@ class Test_Defender_Service: identity=M365IdentityInfo(tenant_domain=DOMAIN) ) ) - antiphishing_rules = defender_client.antiphising_rules + antiphishing_rules = defender_client.antiphishing_rules assert antiphishing_rules["Policy1"].state == "Enabled" + assert antiphishing_rules["Policy1"].priority == 1 + assert antiphishing_rules["Policy1"].users == ["test@example.com"] + assert antiphishing_rules["Policy1"].groups == ["example_group"] + assert antiphishing_rules["Policy1"].domains == ["example.com"] assert antiphishing_rules["Policy2"].state == "Disabled" + assert antiphishing_rules["Policy2"].priority == 2 + assert antiphishing_rules["Policy2"].users == ["test@example.com"] + assert antiphishing_rules["Policy2"].groups == ["example_group"] + assert antiphishing_rules["Policy2"].domains == ["example.com"] defender_client.powershell.close() @patch( @@ -357,6 +429,7 @@ class Test_Defender_Service: ) ) outbound_spam_policies = defender_client.outbound_spam_policies + assert outbound_spam_policies["Policy1"].name == "Policy1" assert outbound_spam_policies["Policy1"].notify_sender_blocked is True assert outbound_spam_policies["Policy1"].notify_limit_exceeded is True assert outbound_spam_policies[ @@ -367,6 +440,7 @@ class Test_Defender_Service: ].notify_sender_blocked_addresses == ["security@example.com"] assert outbound_spam_policies["Policy1"].auto_forwarding_mode is False assert outbound_spam_policies["Policy1"].default is False + assert outbound_spam_policies["Policy2"].name == "Policy2" assert outbound_spam_policies["Policy2"].notify_sender_blocked is False assert outbound_spam_policies["Policy2"].notify_limit_exceeded is False assert ( @@ -377,6 +451,7 @@ class Test_Defender_Service: ) assert outbound_spam_policies["Policy2"].auto_forwarding_mode is True assert outbound_spam_policies["Policy2"].default is True + defender_client.powershell.close() @patch( "prowler.providers.m365.services.defender.defender_service.Defender._get_outbound_spam_filter_rule", @@ -395,7 +470,16 @@ class Test_Defender_Service: ) outbound_spam_rules = defender_client.outbound_spam_rules assert outbound_spam_rules["Policy1"].state == "Enabled" + assert outbound_spam_rules["Policy1"].priority == 1 + assert outbound_spam_rules["Policy1"].users == ["test@example.com"] + assert outbound_spam_rules["Policy1"].groups == ["example_group"] + assert outbound_spam_rules["Policy1"].domains == ["example.com"] assert outbound_spam_rules["Policy2"].state == "Disabled" + assert outbound_spam_rules["Policy2"].priority == 2 + assert outbound_spam_rules["Policy2"].users == ["test@example.com"] + assert outbound_spam_rules["Policy2"].groups == ["example_group"] + assert outbound_spam_rules["Policy2"].domains == ["example.com"] + defender_client.powershell.close() @patch( "prowler.providers.m365.services.defender.defender_service.Defender._get_inbound_spam_filter_policy", @@ -417,6 +501,34 @@ class Test_Defender_Service: assert inbound_spam_policies[1].allowed_sender_domains == ["example.com"] defender_client.powershell.close() + @patch( + "prowler.providers.m365.services.defender.defender_service.Defender._get_inbound_spam_filter_rule", + new=mock_defender_get_inbound_spam_rule, + ) + def test__get_inbound_spam_filter_rule(self): + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + defender_client = Defender( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + inbound_spam_rules = defender_client.inbound_spam_rules + assert inbound_spam_rules["Policy1"].state == "Enabled" + assert inbound_spam_rules["Policy1"].priority == 1 + assert inbound_spam_rules["Policy1"].users == ["test@example.com"] + assert inbound_spam_rules["Policy1"].groups == ["example_group"] + assert inbound_spam_rules["Policy1"].domains == ["example.com"] + assert inbound_spam_rules["Policy2"].state == "Disabled" + assert inbound_spam_rules["Policy2"].priority == 2 + assert inbound_spam_rules["Policy2"].users == ["test@example.com"] + assert inbound_spam_rules["Policy2"].groups == ["example_group"] + assert inbound_spam_rules["Policy2"].domains == ["example.com"] + defender_client.powershell.close() + @patch( "prowler.providers.m365.services.defender.defender_service.Defender._get_report_submission_policy", new=mock_defender_get_report_submission_policy,