From a671b092ee143c652d2f9f25ef2ffa066d700b3a Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:15:33 +0200 Subject: [PATCH] feat(defender): add new check `defender_domain_dkim_enabled` (#7485) Co-authored-by: HugoPBrito Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + .../m365/lib/powershell/m365_powershell.py | 18 +++ .../defender_domain_dkim_enabled/__init__.py | 0 ...defender_domain_dkim_enabled.metadata.json | 30 +++++ .../defender_domain_dkim_enabled.py | 45 +++++++ .../services/defender/defender_service.py | 27 ++++ .../defender_domain_dkim_enabled_test.py | 119 ++++++++++++++++++ .../defender/m365_defender_service_test.py | 30 +++++ 8 files changed, 270 insertions(+) create mode 100644 prowler/providers/m365/services/defender/defender_domain_dkim_enabled/__init__.py create mode 100644 prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json create mode 100644 prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py create mode 100644 tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 0773aceeef..975b5d978e 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add service Exchange to Microsoft365 with one check for Organizations Mailbox Auditing enabled [(#7408)](https://github.com/prowler-cloud/prowler/pull/7408) - Add check for Bypass Disable in every Mailbox for service Defender in M365 [(#7418)](https://github.com/prowler-cloud/prowler/pull/7418) - Add new check `teams_email_sending_to_channel_disabled` [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533) +- Add new check for DKIM enabled for service Defender in M365 [(#7485)](https://github.com/prowler-cloud/prowler/pull/7485) ### Fixed diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 573cdd5adf..9e40f8e6a3 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -305,3 +305,21 @@ class M365PowerShell(PowerShellSession): } """ return self.execute("Get-MailboxAuditBypassAssociation | ConvertTo-Json") + + def get_dkim_config(self) -> dict: + """ + Get DKIM Signing Configuration. + + Retrieves the current DKIM signing configuration settings for Exchange Online. + + Returns: + dict: DKIM signing configuration settings in JSON format. + + Example: + >>> get_dkim_config() + { + "Id": "12345678-1234-1234-1234-123456789012", + "Enabled": true + } + """ + return self.execute("Get-DkimSigningConfig | ConvertTo-Json") diff --git a/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/__init__.py b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json new file mode 100644 index 0000000000..61551723be --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "defender_domain_dkim_enabled", + "CheckTitle": "Ensure that DKIM is enabled for all Exchange Online Domains", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Exchange Online Domain", + "Description": "This check ensures that DomainKeys Identified Mail (DKIM) is enabled for all Exchange Online domains. DKIM is a crucial authentication method that, along with SPF and DMARC, helps prevent attackers from sending spoofed emails that appear to originate from your domain. By adding a digital signature to outbound emails, DKIM allows receiving email systems to verify the legitimacy of incoming messages.", + "Risk": "If DKIM is not enabled, attackers may send spoofed emails that appear to originate from your domain, potentially leading to phishing attacks and damage to your domain's reputation.", + "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide", + "Remediation": { + "Code": { + "CLI": "Set-DkimSigningConfig -Identity -Enabled $True", + "NativeIaC": "", + "Other": "1. After DNS records are created, enable DKIM signing in Microsoft 365 Defender. 2. Navigate to Microsoft 365 Defender at https://security.microsoft.com/. 3. Go to Email & collaboration > Policies & rules > Threat policies. 4. Under Rules, select Email authentication settings. 5. Choose DKIM, click on each domain, and enable 'Sign messages for this domain with DKIM signature'.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable DKIM for all your Exchange Online domains to ensure emails are cryptographically signed and to protect against email spoofing.", + "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-dkimsigningconfig?view=exchange-ps" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py new file mode 100644 index 0000000000..f6258744d0 --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py @@ -0,0 +1,45 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.defender.defender_client import defender_client + + +class defender_domain_dkim_enabled(Check): + """ + Check if DKIM is enabled for all Exchange Online domains. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check to verify if DKIM is enabled for all domains. + + This method checks the DKIM signing configuration for each domain to determine if DKIM is enabled. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + for config in defender_client.dkim_configurations: + report = CheckReportM365( + metadata=self.metadata(), + resource=config, + resource_name="DKIM Configuration", + resource_id=config.id, + ) + report.status = "FAIL" + report.status_extended = ( + f"DKIM is not enabled for domain with ID {config.id}." + ) + + if config.dkim_signing_enabled: + report.status = "PASS" + report.status_extended = ( + f"DKIM is enabled for domain with ID {config.id}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/defender/defender_service.py b/prowler/providers/m365/services/defender/defender_service.py index 05e57d669d..3fd256a085 100644 --- a/prowler/providers/m365/services/defender/defender_service.py +++ b/prowler/providers/m365/services/defender/defender_service.py @@ -16,6 +16,7 @@ class Defender(M365Service): 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.dkim_configurations = self._get_dkim_config() self.powershell.close() def _get_malware_filter_policy(self): @@ -93,6 +94,27 @@ class Defender(M365Service): ) return antiphishing_rules + def _get_dkim_config(self): + logger.info("Microsoft365 - Getting DKIM settings...") + dkim_configs = [] + try: + dkim_config = self.powershell.get_dkim_config() + if isinstance(dkim_config, dict): + dkim_config = [dkim_config] + for config in dkim_config: + if config: + dkim_configs.append( + DkimConfig( + dkim_signing_enabled=config.get("Enabled", False), + id=config.get("Id", ""), + ) + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return dkim_configs + def _get_outbound_spam_filter_policy(self): logger.info("Microsoft365 - Getting Defender outbound spam filter policy...") outbound_spam_policies = {} @@ -163,6 +185,11 @@ class AntiphishingRule(BaseModel): state: str +class DkimConfig(BaseModel): + dkim_signing_enabled: bool + id: str + + class OutboundSpamPolicy(BaseModel): notify_sender_blocked: bool notify_limit_exceeded: bool diff --git a/tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py b/tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py new file mode 100644 index 0000000000..bbbf82b51c --- /dev/null +++ b/tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py @@ -0,0 +1,119 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_defender_domain_dkim_enabled: + def test_dkim_enabled(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_domain_dkim_enabled.defender_domain_dkim_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_domain_dkim_enabled.defender_domain_dkim_enabled import ( + defender_domain_dkim_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DkimConfig, + ) + + defender_client.dkim_configurations = [ + DkimConfig(dkim_signing_enabled=True, id="domain1") + ] + + check = defender_domain_dkim_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "DKIM is enabled for domain with ID domain1." + ) + assert result[0].resource == defender_client.dkim_configurations[0].dict() + assert result[0].resource_name == "DKIM Configuration" + assert result[0].resource_id == "domain1" + assert result[0].location == "global" + + def test_dkim_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_domain_dkim_enabled.defender_domain_dkim_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_domain_dkim_enabled.defender_domain_dkim_enabled import ( + defender_domain_dkim_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DkimConfig, + ) + + defender_client.dkim_configurations = [ + DkimConfig(dkim_signing_enabled=False, id="domain2") + ] + + check = defender_domain_dkim_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "DKIM is not enabled for domain with ID domain2." + ) + assert result[0].resource == defender_client.dkim_configurations[0].dict() + assert result[0].resource_name == "DKIM Configuration" + assert result[0].resource_id == "domain2" + assert result[0].location == "global" + + def test_no_dkim_configurations(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_domain_dkim_enabled.defender_domain_dkim_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_domain_dkim_enabled.defender_domain_dkim_enabled import ( + defender_domain_dkim_enabled, + ) + + defender_client.dkim_configurations = [] + + check = defender_domain_dkim_enabled() + result = check.execute() + 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 6a9b881d31..3738f30a94 100644 --- a/tests/providers/m365/services/defender/m365_defender_service_test.py +++ b/tests/providers/m365/services/defender/m365_defender_service_test.py @@ -6,6 +6,7 @@ from prowler.providers.m365.services.defender.defender_service import ( AntiphishingPolicy, AntiphishingRule, Defender, + DkimConfig, MalwarePolicy, OutboundSpamPolicy, OutboundSpamRule, @@ -68,6 +69,13 @@ def mock_defender_get_antiphising_rules(_): } +def mock_defender_get_dkim_config(_): + return [ + DkimConfig(dkim_signing_enabled=True, id="domain1"), + DkimConfig(dkim_signing_enabled=False, id="domain2"), + ] + + def mock_defender_get_outbound_spam_filter_policy(_): return { "Policy1": OutboundSpamPolicy( @@ -210,6 +218,28 @@ class Test_Defender_Service: assert antiphishing_rules["Policy2"].state == "Disabled" defender_client.powershell.close() + @patch( + "prowler.providers.m365.services.defender.defender_service.Defender._get_dkim_config", + new=mock_defender_get_dkim_config, + ) + def test_get_dkim_config(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) + ) + ) + dkim_configs = defender_client.dkim_configurations + assert dkim_configs[0].dkim_signing_enabled is True + assert dkim_configs[0].id == "domain1" + assert dkim_configs[1].dkim_signing_enabled is False + assert dkim_configs[1].id == "domain2" + defender_client.powershell.close() + @patch( "prowler.providers.m365.services.defender.defender_service.Defender._get_outbound_spam_filter_policy", new=mock_defender_get_outbound_spam_filter_policy,