Compare commits

...
Author SHA1 Message Date
Rubén De la Torre Vico dbab48ea97 docs: add changelog entry for gmail_dkim_enabled_all_domains check 2026-05-28 11:24:59 +02:00
Rubén De la Torre Vico a74ff0f8f4 feat(googleworkspace): add gmail_dkim_enabled_all_domains security check
Add new security check gmail_dkim_enabled_all_domains for googleworkspace provider.
Includes check implementation, metadata, and unit tests.
2026-05-28 11:24:20 +02:00
8 changed files with 273 additions and 2 deletions
+1
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `gmail_dkim_enabled_all_domains` check for googleworkspace provider [(#11381)](https://github.com/prowler-cloud/prowler/pull/11381)
- `application` service for Okta provider with `application_admin_console_session_idle_timeout_15min`, `application_admin_console_mfa_required`, `application_admin_console_phishing_resistant_authentication`, `application_dashboard_mfa_required`, `application_dashboard_phishing_resistant_authentication`, and `application_authentication_policy_network_zone_enforced` checks [(#11358)](https://github.com/prowler-cloud/prowler/pull/11358)
- AWS AI Security Framework compliance for AWS provider [(#11353)](https://github.com/prowler-cloud/prowler/pull/11353)
- `storage_account_public_network_access_disabled` check for Azure provider and remapped the Azure CIS "Public Network Access is Disabled" requirements to it [(#11334)](https://github.com/prowler-cloud/prowler/pull/11334)
@@ -569,7 +569,9 @@
{
"Id": "3.1.3.2.1",
"Description": "Ensure that DKIM is enabled for all mail enabled domains",
"Checks": [],
"Checks": [
"gmail_dkim_enabled_all_domains"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -600,7 +600,9 @@
{
"Id": "GWS.GMAIL.2.1",
"Description": "DKIM SHOULD be enabled for all domains",
"Checks": [],
"Checks": [
"gmail_dkim_enabled_all_domains"
],
"Attributes": [
{
"Section": "Gmail",
@@ -0,0 +1,41 @@
{
"Provider": "googleworkspace",
"CheckID": "gmail_dkim_enabled_all_domains",
"CheckTitle": "DKIM ensures email authenticity and integrity for all mail-enabled domains",
"CheckType": [],
"ServiceName": "gmail",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "DKIM (DomainKeys Identified Mail) adds a **cryptographic signature** to outgoing email headers, allowing receiving servers to verify that messages originate from the domain and were not altered in transit.\n\nThis check evaluates whether every mail-enabled domain in the Google Workspace tenant has a DKIM signing key generated and authentication started.",
"Risk": "Without DKIM, attackers can more easily **spoof the organization's domains**, increasing the risk of **phishing, business email compromise, and reduced mail deliverability**. Receiving servers that require DKIM may reject or flag legitimate messages, harming trusted communication.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/174124",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Authenticate email**\n4. Select the domain to configure\n5. Click **Generate new record** (select 2048-bit key length if your DNS host supports it)\n6. Add the generated TXT record to your domain's DNS configuration at the selector shown (default: `google._domainkey`)\n7. Wait for DNS propagation, then click **Start authentication**\n8. Repeat for every mail-enabled domain and secondary domain",
"Terraform": ""
},
"Recommendation": {
"Text": "Generate and activate a DKIM signing key for every mail-enabled domain. Use a **2048-bit key** where the DNS host supports it to ensure stronger cryptographic protection against message forgery.",
"Url": "https://hub.prowler.com/check/gmail_dkim_enabled_all_domains"
}
},
"Categories": [
"email-security"
],
"DependsOn": [],
"RelatedTo": [
"gmail_domain_spoofing_protection_enabled",
"gmail_inbound_domain_spoofing_protection_enabled",
"gmail_unauthenticated_email_protection_enabled"
],
"Notes": "This check is Manual because no public Google Admin SDK/API endpoint exposes the Gmail DKIM authentication status. Verification requires inspecting the Admin Console and performing a DNS TXT record lookup (e.g., dig TXT google._domainkey.<domain>)."
}
@@ -0,0 +1,51 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client
class gmail_dkim_enabled_all_domains(Check):
"""Verify that DKIM is enabled for all mail-enabled domains.
DKIM (DomainKeys Identified Mail) adds a cryptographic signature to
outgoing messages, allowing receivers to verify authenticity and integrity.
Because there is no public Admin SDK/API endpoint to query DKIM status,
this check always returns MANUAL and directs the administrator to verify
DKIM configuration in the Google Admin Console and via DNS lookup.
- MANUAL: DKIM authentication status must be verified manually in the
Admin Console and through DNS TXT record inspection for each domain.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
"""Execute the DKIM enabled check.
Returns:
A list of reports with MANUAL status requiring administrator
verification of DKIM configuration per domain.
"""
findings = []
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=gmail_client.policies,
resource_id="gmailPolicies",
resource_name="Gmail Policies",
customer_id=gmail_client.provider.identity.customer_id,
)
report.status = "MANUAL"
report.status_extended = (
f"DKIM authentication status for domain "
f"{gmail_client.provider.identity.domain} cannot be automatically "
f"verified because no public Admin SDK/API endpoint exposes this "
f"setting. Verify in the Admin Console under Apps > Google "
f"Workspace > Gmail > Authenticate email that DKIM signing is "
f"generated and authentication is started for every mail-enabled "
f"domain, and confirm via DNS that a valid TXT record exists at "
f"google._domainkey.{gmail_client.provider.identity.domain}."
)
findings.append(report)
return findings
@@ -0,0 +1,174 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.models import GoogleWorkspaceIdentityInfo
from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
DELEGATED_USER,
DOMAIN,
ROOT_ORG_UNIT_ID,
set_mocked_googleworkspace_provider,
)
class TestGmailDkimEnabledAllDomains:
"""Tests for the gmail_dkim_enabled_all_domains check.
Since DKIM status is not exposed through any public Admin SDK/API,
this check always returns MANUAL to prompt administrator verification.
"""
def test_manual_status(self):
"""Check always returns MANUAL because DKIM status cannot be queried via API."""
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains.gmail_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains import (
gmail_dkim_enabled_all_domains,
)
mock_client.provider = mock_provider
mock_client.policies = GmailPolicies()
check = gmail_dkim_enabled_all_domains()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "MANUAL"
assert findings[0].resource_name == "Gmail Policies"
assert findings[0].resource_id == "gmailPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert findings[0].resource == GmailPolicies().dict()
def test_manual_status_extended_contains_domain(self):
"""Verify the status_extended message references the tenant domain."""
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains.gmail_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains import (
gmail_dkim_enabled_all_domains,
)
mock_client.provider = mock_provider
mock_client.policies = GmailPolicies()
check = gmail_dkim_enabled_all_domains()
findings = check.execute()
assert len(findings) == 1
assert DOMAIN in findings[0].status_extended
assert "cannot be automatically verified" in findings[0].status_extended
assert f"google._domainkey.{DOMAIN}" in findings[0].status_extended
def test_manual_status_extended_contains_admin_console_guidance(self):
"""Verify the status_extended message includes Admin Console verification instructions."""
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains.gmail_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains import (
gmail_dkim_enabled_all_domains,
)
mock_client.provider = mock_provider
mock_client.policies = GmailPolicies()
check = gmail_dkim_enabled_all_domains()
findings = check.execute()
assert len(findings) == 1
assert "Admin Console" in findings[0].status_extended
assert "Authenticate email" in findings[0].status_extended
assert "DKIM" in findings[0].status_extended
def test_manual_status_with_custom_domain(self):
"""Verify the check correctly references a custom domain in the output."""
custom_domain = "custom-org.io"
custom_customer_id = "C9876543"
mock_provider = set_mocked_googleworkspace_provider(
identity=GoogleWorkspaceIdentityInfo(
domain=custom_domain,
customer_id=custom_customer_id,
delegated_user=f"admin@{custom_domain}",
root_org_unit_id=ROOT_ORG_UNIT_ID,
profile="default",
),
)
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains.gmail_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains import (
gmail_dkim_enabled_all_domains,
)
mock_client.provider = mock_provider
mock_client.policies = GmailPolicies()
check = gmail_dkim_enabled_all_domains()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "MANUAL"
assert findings[0].customer_id == custom_customer_id
assert custom_domain in findings[0].status_extended
assert f"google._domainkey.{custom_domain}" in findings[0].status_extended
# Ensure default domain is NOT referenced
assert DOMAIN not in findings[0].status_extended
def test_single_finding_returned(self):
"""Check always produces exactly one finding (one MANUAL report per execution)."""
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains.gmail_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.gmail.gmail_dkim_enabled_all_domains.gmail_dkim_enabled_all_domains import (
gmail_dkim_enabled_all_domains,
)
mock_client.provider = mock_provider
mock_client.policies = GmailPolicies()
check = gmail_dkim_enabled_all_domains()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status != "PASS"
assert findings[0].status != "FAIL"
assert findings[0].status == "MANUAL"