diff --git a/prowler/changelog.d/googleworkspace-policy-precedence.fixed.md b/prowler/changelog.d/googleworkspace-policy-precedence.fixed.md new file mode 100644 index 0000000000..20f08f766f --- /dev/null +++ b/prowler/changelog.d/googleworkspace-policy-precedence.fixed.md @@ -0,0 +1 @@ +Google Workspace checks now read the policy Google applies (greatest sortOrder) instead of the last policy the Cloud Identity Policy API lists, which reported default values over admin-configured ones diff --git a/prowler/changelog.d/googleworkspace-service-off.changed.md b/prowler/changelog.d/googleworkspace-service-off.changed.md new file mode 100644 index 0000000000..a1c578750f --- /dev/null +++ b/prowler/changelog.d/googleworkspace-service-off.changed.md @@ -0,0 +1 @@ +Google Workspace checks for Chat, Drive, Gmail, Calendar and Groups pass with the reason stated when the app is turned off for everyone and no organizational unit or group turns it back on. The rule covers settings that only act while the app is on: Drive sharing settings and the settings of existing groups keep being evaluated because existing shares and groups stay reachable, and only group creation is exempt when Groups for Business is off diff --git a/prowler/providers/googleworkspace/lib/service/service.py b/prowler/providers/googleworkspace/lib/service/service.py index 35a10454cd..0c9449801c 100644 --- a/prowler/providers/googleworkspace/lib/service/service.py +++ b/prowler/providers/googleworkspace/lib/service/service.py @@ -1,5 +1,8 @@ +from typing import Optional + from googleapiclient.discovery import build from googleapiclient.errors import HttpError +from pydantic import BaseModel from prowler.lib.logger import logger from prowler.providers.googleworkspace.googleworkspace_provider import ( @@ -11,6 +14,52 @@ CUSTOMER_SCOPE = "customer" OVERRIDE_SCOPE = "override" UNKNOWN_SCOPE = "unknown" +# Settings whose policies are items of a list rather than competing values for +# one setting: every rule is a distinct policy and none outranks another. +LIST_SETTING_NAMESPACES = ("rule.",) + + +def _setting_type(policy: dict) -> str: + """Return the setting type of a policy without the "settings/" prefix.""" + return (policy.get("setting") or {}).get("type", "").removeprefix("settings/") + + +def _service_state(policy: dict) -> Optional[str]: + """Return the serviceState a service status policy carries, if any.""" + return (((policy.get("setting") or {}).get("value")) or {}).get("serviceState") + + +def _sort_order(policy: dict) -> float: + """Return the policy's sortOrder, or 0 when the API did not give one.""" + try: + return float((policy.get("policyQuery") or {}).get("sortOrder") or 0) + except (TypeError, ValueError): + return 0.0 + + +class ServicePolicies(BaseModel): + """Base of the per-service policy models: carries the service status. + + `service_state` is the customer-level value of the service's + `.service_status` setting. `service_state_overridden` records + that an organizational unit or group turns the service back on, in + which case "off" at the customer level does not mean off for everyone. + """ + + service_state: Optional[str] = None + service_state_overridden: bool = False + + @property + def off_for_everyone(self) -> bool: + """Whether the service is off at the customer level with no override. + + Settings that only act while the service is on have no effect then, + so the checks on them pass with that reason. Settings that govern + what already exists, such as Drive shares or Groups, keep being + evaluated: Google keeps existing shares and groups reachable. + """ + return self.service_state == "DISABLED" and not self.service_state_overridden + class GoogleWorkspaceService: def __init__( @@ -71,6 +120,88 @@ class GoogleWorkspaceService: """Whether a policy applies to the whole domain.""" return self._policy_scope(policy) == CUSTOMER_SCOPE + def _list_policies( + self, service, filter_str: str, context: str + ) -> tuple[list[dict], bool]: + """Read every Cloud Identity policy matching a filter, across pages. + + Returns the policies read and whether every page was read. A failing + page is logged, the pages read before it are kept, and the caller + decides what a partial read means for its checks. + """ + policies: list[dict] = [] + try: + request = service.policies().list(pageSize=100, filter=filter_str) + while request is not None: + response = request.execute() + policies.extend(response.get("policies", [])) + request = service.policies().list_next(request, response) + except Exception as error: + self._handle_api_error(error, context, self.provider.identity.customer_id) + return policies, False + return policies, True + + def _effective_policies(self, policies: list[dict]) -> list[dict]: + """Keep, per setting type, the customer-level policy Google applies. + + The API returns every policy that exists for a setting: Google's + SYSTEM defaults next to the ADMIN policy saved in the Admin console. + The value in force is the one with the greatest policyQuery.sortOrder + (Policy API concepts, Max and Merge reducers), not the last one + listed. Policies without a sort order keep the API order, so the last + one wins. Rules are not reduced: each policy is a distinct rule. + """ + kept: list[dict] = [] + position: dict[str, int] = {} + for policy in policies: + if not self._is_customer_level_policy(policy): + continue + setting_type = _setting_type(policy) + if setting_type.startswith(LIST_SETTING_NAMESPACES): + kept.append(policy) + continue + index = position.get(setting_type) + if index is None: + position[setting_type] = len(kept) + kept.append(policy) + elif _sort_order(policy) >= _sort_order(kept[index]): + kept[index] = policy + return kept + + def _service_status( + self, policies: list[dict], setting_type: str + ) -> tuple[Optional[str], bool]: + """Return a service's customer-level state and whether anything overrides it. + + The state comes from the effective customer-level policy of the given + `.service_status` setting. A policy of that setting outside + the customer scope counts as an override when it turns the service on, + or when its scope could not be resolved: "off" then holds only for + part of the tenant. + """ + status_policies = [ + policy for policy in policies if _setting_type(policy) == setting_type + ] + overridden = any( + self._overrides_service_status(policy) for policy in status_policies + ) + effective = self._effective_policies(status_policies) + state = _service_state(effective[0]) if effective else None + return state, overridden + + def _overrides_service_status(self, policy: dict) -> bool: + """Whether a status policy turns the service back on for part of the tenant. + + A sub-OU or group policy counts only when it does not say DISABLED: an + OU that also turns the service off changes nothing, and Google lets an + access group only turn a service on. A policy whose scope cannot be + resolved counts as well, since it may be either. + """ + scope = self._policy_scope(policy) + if scope == CUSTOMER_SCOPE: + return False + return scope == UNKNOWN_SCOPE or _service_state(policy) != "DISABLED" + def _handle_api_error(self, error, context: str, resource_name: str = ""): """ Centralized Google Workspace API error handling. diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py index 5a41060bc8..c3ac55ea8b 100644 --- a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py @@ -31,43 +31,23 @@ class AdditionalServices(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("groups.service_status")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("groups.service_status")', + "fetching Additional Services policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - value = setting.get("value", {}) - - if setting_type == "groups.service_status": - self.policies.groups_service_state = value.get( - "serviceState" - ) - logger.debug( - "Additional Services - Groups service state: " - f"{self.policies.groups_service_state}" - ) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching Additional Services policies", - self.provider.identity.customer_id, + if setting_type == "groups.service_status": + self.policies.groups_service_state = value.get("serviceState") + logger.debug( + "Additional Services - Groups service state: " + f"{self.policies.groups_service_state}" ) - fetch_succeeded = False - break self.policies_fetched = fetch_succeeded diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json index 3cca0005dd..befbe63f7f 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json @@ -36,5 +36,5 @@ "calendar_external_sharing_primary_calendar", "calendar_external_sharing_secondary_calendar" ], - "Notes": "" + "Notes": "When Google Calendar is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py index f2c8f58b2f..e2c7767f90 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py @@ -28,7 +28,13 @@ class calendar_external_invitations_warning(Check): warning_enabled = calendar_client.policies.external_invitations_warning - if warning_enabled is True: + if calendar_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Calendar is turned off for everyone in domain " + f"{calendar_client.provider.identity.domain}, so no invitation can be sent." + ) + elif warning_enabled is True: report.status = "PASS" report.status_extended = ( f"External invitation warnings for Google Calendar are enabled " diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json index d923754a9e..a041167f1f 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json @@ -36,5 +36,5 @@ "calendar_external_sharing_secondary_calendar", "calendar_external_invitations_warning" ], - "Notes": "" + "Notes": "When Google Calendar is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py index 42be5e9f0d..f50cda84d2 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py @@ -28,7 +28,13 @@ class calendar_external_sharing_primary_calendar(Check): sharing = calendar_client.policies.primary_calendar_external_sharing - if sharing == "EXTERNAL_FREE_BUSY_ONLY": + if calendar_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Calendar is turned off for everyone in domain " + f"{calendar_client.provider.identity.domain}, so no calendar can be shared outside the organization." + ) + elif sharing == "EXTERNAL_FREE_BUSY_ONLY": report.status = "PASS" report.status_extended = ( f"Primary calendar external sharing in domain " diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json index d1ac914a0f..222ac4fae2 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json @@ -36,5 +36,5 @@ "calendar_external_sharing_primary_calendar", "calendar_external_invitations_warning" ], - "Notes": "" + "Notes": "When Google Calendar is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py index 5f53fd2089..7e857f5afc 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py @@ -28,7 +28,13 @@ class calendar_external_sharing_secondary_calendar(Check): sharing = calendar_client.policies.secondary_calendar_external_sharing - if sharing == "EXTERNAL_FREE_BUSY_ONLY": + if calendar_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Calendar is turned off for everyone in domain " + f"{calendar_client.provider.identity.domain}, so no calendar can be shared outside the organization." + ) + elif sharing == "EXTERNAL_FREE_BUSY_ONLY": report.status = "PASS" report.status_extended = ( f"Secondary calendar external sharing in domain " diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_service.py b/prowler/providers/googleworkspace/services/calendar/calendar_service.py index aa822d4218..a8d75d86ea 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_service.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_service.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel - from prowler.lib.logger import logger -from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService +from prowler.providers.googleworkspace.lib.service.service import ( + GoogleWorkspaceService, + ServicePolicies, +) class Calendar(GoogleWorkspaceService): @@ -30,66 +31,21 @@ class Calendar(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("calendar.*")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("calendar.*")', + "fetching calendar policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + ( + self.policies.service_state, + self.policies.service_state_overridden, + ) = self._service_status(policies, "calendar.service_status") - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - value = setting.get("value", {}) - - if ( - setting_type - == "calendar.primary_calendar_max_allowed_external_sharing" - ): - self.policies.primary_calendar_external_sharing = value.get( - "maxAllowedExternalSharing" - ) - logger.debug( - "Primary calendar external sharing: " - f"{self.policies.primary_calendar_external_sharing}" - ) - - elif ( - setting_type - == "calendar.secondary_calendar_max_allowed_external_sharing" - ): - self.policies.secondary_calendar_external_sharing = ( - value.get("maxAllowedExternalSharing") - ) - logger.debug( - "Secondary calendar external sharing: " - f"{self.policies.secondary_calendar_external_sharing}" - ) - - elif setting_type == "calendar.external_invitations": - self.policies.external_invitations_warning = value.get( - "warnOnInvite" - ) - logger.debug( - "External invitations warning: " - f"{self.policies.external_invitations_warning}" - ) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching calendar policies", - self.provider.identity.customer_id, - ) - fetch_succeeded = False - break + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + self._process_setting(setting_type, setting.get("value", {})) self.policies_fetched = fetch_succeeded @@ -108,8 +64,35 @@ class Calendar(GoogleWorkspaceService): ) self.policies_fetched = False + def _process_setting(self, setting_type: str, value: dict): + """Populate the model from a single policy setting.""" + if setting_type == "calendar.primary_calendar_max_allowed_external_sharing": + self.policies.primary_calendar_external_sharing = value.get( + "maxAllowedExternalSharing" + ) + logger.debug( + "Primary calendar external sharing: " + f"{self.policies.primary_calendar_external_sharing}" + ) -class CalendarPolicies(BaseModel): + elif setting_type == "calendar.secondary_calendar_max_allowed_external_sharing": + self.policies.secondary_calendar_external_sharing = value.get( + "maxAllowedExternalSharing" + ) + logger.debug( + "Secondary calendar external sharing: " + f"{self.policies.secondary_calendar_external_sharing}" + ) + + elif setting_type == "calendar.external_invitations": + self.policies.external_invitations_warning = value.get("warnOnInvite") + logger.debug( + "External invitations warning: " + f"{self.policies.external_invitations_warning}" + ) + + +class CalendarPolicies(ServicePolicies): """Model for domain-level Calendar policy settings.""" primary_calendar_external_sharing: Optional[str] = None diff --git a/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.metadata.json index 070de032e1..9b816cee15 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.metadata.json @@ -35,5 +35,5 @@ "RelatedTo": [ "chat_incoming_webhooks_disabled" ], - "Notes": "" + "Notes": "When Google Chat is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.py index c80be7e6dc..16246e1d87 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.py +++ b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.py @@ -26,7 +26,13 @@ class chat_apps_installation_disabled(Check): apps_enabled = chat_client.policies.enable_apps - if apps_enabled is False: + if chat_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Chat is turned off for everyone in domain " + f"{chat_client.provider.identity.domain}, so Chat apps cannot be installed." + ) + elif apps_enabled is False: report.status = "PASS" report.status_extended = ( f"Chat apps installation is disabled " diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.metadata.json index adc1b1fc5f..e8811ffbc1 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.metadata.json @@ -36,5 +36,5 @@ "chat_internal_file_sharing_disabled", "drive_sharing_allowlisted_domains" ], - "Notes": "" + "Notes": "When Google Chat is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.py index 7c17b314e2..68a6a060df 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.py +++ b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.py @@ -26,7 +26,13 @@ class chat_external_file_sharing_disabled(Check): external_sharing = chat_client.policies.external_file_sharing - if external_sharing == "NO_FILES": + if chat_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Chat is turned off for everyone in domain " + f"{chat_client.provider.identity.domain}, so no files can be shared externally through Chat." + ) + elif external_sharing == "NO_FILES": report.status = "PASS" report.status_extended = ( f"External file sharing in Chat is disabled " diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.metadata.json index 34b0016065..354ec81993 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.metadata.json +++ b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.metadata.json @@ -36,5 +36,5 @@ "chat_external_spaces_restricted", "drive_sharing_allowlisted_domains" ], - "Notes": "" + "Notes": "When Google Chat is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.py b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.py index aef75403b9..a9836cbb82 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.py +++ b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.py @@ -27,7 +27,13 @@ class chat_external_messaging_restricted(Check): allow_external = chat_client.policies.allow_external_chat restriction = chat_client.policies.external_chat_restriction - if allow_external is False: + if chat_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Chat is turned off for everyone in domain " + f"{chat_client.provider.identity.domain}, so no external messaging can take place." + ) + elif allow_external is False: report.status = "PASS" report.status_extended = ( f"External Chat messaging is disabled " diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.metadata.json index 1385f34ef8..cdf2820401 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.metadata.json +++ b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.metadata.json @@ -36,5 +36,5 @@ "chat_external_messaging_restricted", "drive_sharing_allowlisted_domains" ], - "Notes": "" + "Notes": "When Google Chat is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.py b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.py index d6ad421bda..66bc2f6a32 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.py +++ b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.py @@ -27,7 +27,13 @@ class chat_external_spaces_restricted(Check): spaces_enabled = chat_client.policies.external_spaces_enabled allowlist_mode = chat_client.policies.external_spaces_domain_allowlist_mode - if spaces_enabled is False: + if chat_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Chat is turned off for everyone in domain " + f"{chat_client.provider.identity.domain}, so no external spaces can be created or joined." + ) + elif spaces_enabled is False: report.status = "PASS" report.status_extended = ( f"External spaces are disabled " diff --git a/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.metadata.json index 8e8f4b6301..087eea82e1 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.metadata.json @@ -35,5 +35,5 @@ "RelatedTo": [ "chat_apps_installation_disabled" ], - "Notes": "" + "Notes": "When Google Chat is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.py index 753daf02da..e04cb2be9c 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.py +++ b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.py @@ -26,7 +26,13 @@ class chat_incoming_webhooks_disabled(Check): webhooks_enabled = chat_client.policies.enable_webhooks - if webhooks_enabled is False: + if chat_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Chat is turned off for everyone in domain " + f"{chat_client.provider.identity.domain}, so incoming webhooks cannot be used." + ) + elif webhooks_enabled is False: report.status = "PASS" report.status_extended = ( f"Incoming webhooks are disabled " diff --git a/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.metadata.json index b41f662cc3..b6969633be 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.metadata.json @@ -35,5 +35,5 @@ "RelatedTo": [ "chat_external_file_sharing_disabled" ], - "Notes": "" + "Notes": "When Google Chat is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.py index 614750ecda..6eb8e5e9a0 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.py +++ b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.py @@ -26,7 +26,13 @@ class chat_internal_file_sharing_disabled(Check): internal_sharing = chat_client.policies.internal_file_sharing - if internal_sharing == "NO_FILES": + if chat_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Chat is turned off for everyone in domain " + f"{chat_client.provider.identity.domain}, so no files can be shared through Chat." + ) + elif internal_sharing == "NO_FILES": report.status = "PASS" report.status_extended = ( f"Internal file sharing in Chat is disabled " diff --git a/prowler/providers/googleworkspace/services/chat/chat_service.py b/prowler/providers/googleworkspace/services/chat/chat_service.py index 92d4d77dfa..1ff6752253 100644 --- a/prowler/providers/googleworkspace/services/chat/chat_service.py +++ b/prowler/providers/googleworkspace/services/chat/chat_service.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel - from prowler.lib.logger import logger -from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService +from prowler.providers.googleworkspace.lib.service.service import ( + GoogleWorkspaceService, + ServicePolicies, +) class Chat(GoogleWorkspaceService): @@ -30,68 +31,21 @@ class Chat(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("chat.*")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("chat.*")', + "fetching Chat policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + ( + self.policies.service_state, + self.policies.service_state_overridden, + ) = self._service_status(policies, "chat.service_status") - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - logger.debug(f"Processing setting type: {setting_type}") - - value = setting.get("value", {}) - - if setting_type == "chat.chat_file_sharing": - self.policies.external_file_sharing = value.get( - "externalFileSharing" - ) - self.policies.internal_file_sharing = value.get( - "internalFileSharing" - ) - logger.debug("Chat file sharing settings fetched.") - - elif setting_type == "chat.external_chat_restriction": - self.policies.allow_external_chat = value.get( - "allowExternalChat" - ) - self.policies.external_chat_restriction = value.get( - "externalChatRestriction" - ) - logger.debug( - "Chat external chat restriction settings fetched." - ) - - elif setting_type == "chat.chat_external_spaces": - self.policies.external_spaces_enabled = value.get("enabled") - self.policies.external_spaces_domain_allowlist_mode = ( - value.get("domainAllowlistMode") - ) - logger.debug("Chat external spaces settings fetched.") - - elif setting_type == "chat.chat_apps_access": - self.policies.enable_apps = value.get("enableApps") - self.policies.enable_webhooks = value.get("enableWebhooks") - logger.debug("Chat apps access settings fetched.") - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching Chat policies", - self.provider.identity.customer_id, - ) - fetch_succeeded = False - break + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + self._process_setting(setting_type, setting.get("value", {})) self.policies_fetched = fetch_succeeded logger.info("Chat policies fetched successfully.") @@ -104,8 +58,36 @@ class Chat(GoogleWorkspaceService): ) self.policies_fetched = False + def _process_setting(self, setting_type: str, value: dict): + """Populate the model from a single policy setting.""" + logger.debug(f"Processing setting type: {setting_type}") -class ChatPolicies(BaseModel): + if setting_type == "chat.chat_file_sharing": + self.policies.external_file_sharing = value.get("externalFileSharing") + self.policies.internal_file_sharing = value.get("internalFileSharing") + logger.debug("Chat file sharing settings fetched.") + + elif setting_type == "chat.external_chat_restriction": + self.policies.allow_external_chat = value.get("allowExternalChat") + self.policies.external_chat_restriction = value.get( + "externalChatRestriction" + ) + logger.debug("Chat external chat restriction settings fetched.") + + elif setting_type == "chat.chat_external_spaces": + self.policies.external_spaces_enabled = value.get("enabled") + self.policies.external_spaces_domain_allowlist_mode = value.get( + "domainAllowlistMode" + ) + logger.debug("Chat external spaces settings fetched.") + + elif setting_type == "chat.chat_apps_access": + self.policies.enable_apps = value.get("enableApps") + self.policies.enable_webhooks = value.get("enableWebhooks") + logger.debug("Chat apps access settings fetched.") + + +class ChatPolicies(ServicePolicies): """Model for domain-level Chat policy settings.""" # chat.chat_file_sharing diff --git a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.metadata.json index 8f1c14510a..4ace882579 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.metadata.json +++ b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.metadata.json @@ -36,5 +36,5 @@ "drive_external_sharing_warn_users", "drive_publishing_files_disabled" ], - "Notes": "" + "Notes": "When Google Drive is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py index 435e9c0f7d..6be92a92b0 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py +++ b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py @@ -27,7 +27,13 @@ class drive_access_checker_recipients_only(Check): access_checker = drive_client.policies.access_checker_suggestions - if access_checker == "RECIPIENTS_ONLY": + if drive_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Drive is turned off for everyone in domain " + f"{drive_client.provider.identity.domain}, so Access Checker cannot widen any share." + ) + elif access_checker == "RECIPIENTS_ONLY": report.status = "PASS" report.status_extended = ( f"Drive and Docs Access Checker in domain " diff --git a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.metadata.json index 576f78b19f..f357ffc6e2 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.metadata.json @@ -31,5 +31,5 @@ "Categories": [], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "When Google Drive is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py index 189694adaf..c13d3a49e6 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py +++ b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py @@ -28,7 +28,13 @@ class drive_desktop_access_disabled(Check): allow_desktop = drive_client.policies.allow_drive_for_desktop - if allow_desktop is False: + if drive_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Drive is turned off for everyone in domain " + f"{drive_client.provider.identity.domain}, so Drive for desktop cannot sync any content." + ) + elif allow_desktop is False: report.status = "PASS" report.status_extended = ( f"Google Drive for desktop is disabled in domain " diff --git a/prowler/providers/googleworkspace/services/drive/drive_service.py b/prowler/providers/googleworkspace/services/drive/drive_service.py index 68c4b48453..4bcebf0857 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_service.py +++ b/prowler/providers/googleworkspace/services/drive/drive_service.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel - from prowler.lib.logger import logger -from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService +from prowler.providers.googleworkspace.lib.service.service import ( + GoogleWorkspaceService, + ServicePolicies, +) class Drive(GoogleWorkspaceService): @@ -31,88 +32,21 @@ class Drive(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("drive_and_docs.*")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("drive_and_docs.*")', + "fetching Drive and Docs policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + ( + self.policies.service_state, + self.policies.service_state_overridden, + ) = self._service_status(policies, "drive_and_docs.service_status") - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - value = setting.get("value", {}) - - if setting_type == "drive_and_docs.external_sharing": - self.policies.external_sharing_mode = value.get( - "externalSharingMode" - ) - self.policies.warn_for_external_sharing = value.get( - "warnForExternalSharing" - ) - self.policies.warn_for_sharing_outside_allowlisted_domains = value.get( - "warnForSharingOutsideAllowlistedDomains" - ) - self.policies.allow_publishing_files = value.get( - "allowPublishingFiles" - ) - self.policies.access_checker_suggestions = value.get( - "accessCheckerSuggestions" - ) - self.policies.allowed_parties_for_distributing_content = ( - value.get("allowedPartiesForDistributingContent") - ) - logger.debug( - "Drive external sharing settings fetched: " - f"mode={self.policies.external_sharing_mode}, " - f"warn={self.policies.warn_for_external_sharing}, " - f"publish={self.policies.allow_publishing_files}" - ) - - elif setting_type == "drive_and_docs.shared_drive_creation": - self.policies.allow_shared_drive_creation = value.get( - "allowSharedDriveCreation" - ) - self.policies.allow_managers_to_override_settings = ( - value.get("allowManagersToOverrideSettings") - ) - self.policies.allow_non_member_access = value.get( - "allowNonMemberAccess" - ) - self.policies.allowed_parties_for_download_print_copy = ( - value.get("allowedPartiesForDownloadPrintCopy") - ) - logger.debug( - "Drive shared drive creation settings fetched: " - f"creation={self.policies.allow_shared_drive_creation}, " - f"managers_override={self.policies.allow_managers_to_override_settings}" - ) - - elif setting_type == "drive_and_docs.drive_for_desktop": - self.policies.allow_drive_for_desktop = value.get( - "allowDriveForDesktop" - ) - logger.debug( - "Drive for desktop setting fetched: " - f"{self.policies.allow_drive_for_desktop}" - ) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching Drive and Docs policies", - self.provider.identity.customer_id, - ) - fetch_succeeded = False - break + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + self._process_setting(setting_type, setting.get("value", {})) self.policies_fetched = fetch_succeeded @@ -131,8 +65,56 @@ class Drive(GoogleWorkspaceService): ) self.policies_fetched = False + def _process_setting(self, setting_type: str, value: dict): + """Populate the model from a single policy setting.""" + if setting_type == "drive_and_docs.external_sharing": + self.policies.external_sharing_mode = value.get("externalSharingMode") + self.policies.warn_for_external_sharing = value.get( + "warnForExternalSharing" + ) + self.policies.warn_for_sharing_outside_allowlisted_domains = value.get( + "warnForSharingOutsideAllowlistedDomains" + ) + self.policies.allow_publishing_files = value.get("allowPublishingFiles") + self.policies.access_checker_suggestions = value.get( + "accessCheckerSuggestions" + ) + self.policies.allowed_parties_for_distributing_content = value.get( + "allowedPartiesForDistributingContent" + ) + logger.debug( + "Drive external sharing settings fetched: " + f"mode={self.policies.external_sharing_mode}, " + f"warn={self.policies.warn_for_external_sharing}, " + f"publish={self.policies.allow_publishing_files}" + ) -class DrivePolicies(BaseModel): + elif setting_type == "drive_and_docs.shared_drive_creation": + self.policies.allow_shared_drive_creation = value.get( + "allowSharedDriveCreation" + ) + self.policies.allow_managers_to_override_settings = value.get( + "allowManagersToOverrideSettings" + ) + self.policies.allow_non_member_access = value.get("allowNonMemberAccess") + self.policies.allowed_parties_for_download_print_copy = value.get( + "allowedPartiesForDownloadPrintCopy" + ) + logger.debug( + "Drive shared drive creation settings fetched: " + f"creation={self.policies.allow_shared_drive_creation}, " + f"managers_override={self.policies.allow_managers_to_override_settings}" + ) + + elif setting_type == "drive_and_docs.drive_for_desktop": + self.policies.allow_drive_for_desktop = value.get("allowDriveForDesktop") + logger.debug( + "Drive for desktop setting fetched: " + f"{self.policies.allow_drive_for_desktop}" + ) + + +class DrivePolicies(ServicePolicies): """Model for domain-level Drive and Docs policy settings.""" # drive_and_docs.external_sharing diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.metadata.json index c27742b7ed..d00167a7f0 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.metadata.json +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.metadata.json @@ -35,5 +35,5 @@ "drive_shared_drive_members_only_access", "drive_shared_drive_disable_download_print_copy" ], - "Notes": "" + "Notes": "When Google Drive is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py index e381ad8f26..291450cc7d 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py @@ -28,7 +28,13 @@ class drive_shared_drive_creation_allowed(Check): allow_creation = drive_client.policies.allow_shared_drive_creation - if allow_creation is True: + if drive_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Google Drive is turned off for everyone in domain " + f"{drive_client.provider.identity.domain}, so no shared drive can be created." + ) + elif allow_creation is True: report.status = "PASS" report.status_extended = ( f"Users in domain {drive_client.provider.identity.domain} " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.metadata.json index 2295591dba..80d4a02612 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.metadata.json @@ -36,5 +36,5 @@ "gmail_encrypted_attachment_protection_enabled", "gmail_script_attachment_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.py index b8895d4e21..f2fad92433 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.py @@ -29,7 +29,13 @@ class gmail_anomalous_attachment_protection_enabled(Check): gmail_client.policies.anomalous_attachment_protection_consequence ) - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against anomalous attachment types in emails " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.metadata.json index 14484c251c..127f228221 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.metadata.json @@ -36,5 +36,5 @@ "gmail_pop_imap_access_disabled", "gmail_per_user_outbound_gateway_disabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py index 65cd6f1fcb..c77ced0bbe 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py @@ -26,7 +26,13 @@ class gmail_auto_forwarding_disabled(Check): forwarding_enabled = gmail_client.policies.enable_auto_forwarding - if forwarding_enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no email can be forwarded." + ) + elif forwarding_enabled is False: report.status = "PASS" report.status_extended = ( f"Automatic email forwarding is disabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.metadata.json index 530d7af908..fbf842267a 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.metadata.json @@ -33,5 +33,5 @@ ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py index 704e5a391a..e8ad593c22 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py @@ -26,7 +26,13 @@ class gmail_comprehensive_mail_storage_enabled(Check): storage_enabled = gmail_client.policies.comprehensive_mail_storage_enabled - if storage_enabled is True: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so there is no Gmail mail to store." + ) + elif storage_enabled is True: report.status = "PASS" report.status_extended = ( f"Comprehensive mail storage is enabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json index 88035f633f..d42cad3be1 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json @@ -38,5 +38,5 @@ "gmail_unauthenticated_email_protection_enabled", "gmail_groups_spoofing_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py index 5d674b60a0..2eb169074f 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py @@ -34,7 +34,13 @@ class gmail_domain_spoofing_protection_enabled(Check): consequence = gmail_client.policies.domain_spoofing_consequence domain = gmail_client.provider.identity.domain - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against domain spoofing based on similar domain names " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json index 103d09ea1e..8aa3977db6 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json @@ -38,5 +38,5 @@ "gmail_unauthenticated_email_protection_enabled", "gmail_groups_spoofing_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py index 8f33bf4d6d..0c9a943780 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py @@ -34,7 +34,13 @@ class gmail_employee_name_spoofing_protection_enabled(Check): consequence = gmail_client.policies.employee_name_spoofing_consequence domain = gmail_client.provider.identity.domain - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against spoofing of employee names " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.metadata.json index 3367e11777..88ae4949c0 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.metadata.json @@ -36,5 +36,5 @@ "gmail_script_attachment_protection_enabled", "gmail_anomalous_attachment_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.py index 30a58cb4b4..f6f84e00f0 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.py @@ -29,7 +29,13 @@ class gmail_encrypted_attachment_protection_enabled(Check): gmail_client.policies.encrypted_attachment_protection_consequence ) - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against encrypted attachments from untrusted " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.metadata.json index 0f14ecd73b..ae37d04145 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.metadata.json @@ -33,5 +33,5 @@ ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py index 33e5a58f9b..52e6ab6110 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py @@ -28,7 +28,13 @@ class gmail_enhanced_pre_delivery_scanning_enabled(Check): gmail_client.policies.enable_enhanced_pre_delivery_scanning ) - if scanning_enabled is True: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif scanning_enabled is True: report.status = "PASS" report.status_extended = ( f"Enhanced pre-delivery message scanning is enabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.metadata.json index 4df5f8bd84..8ae4817b20 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.metadata.json @@ -36,5 +36,5 @@ "gmail_shortener_scanning_enabled", "gmail_untrusted_link_warnings_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py index c71de1459a..c63f9da455 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py @@ -26,7 +26,13 @@ class gmail_external_image_scanning_enabled(Check): scanning_enabled = gmail_client.policies.enable_external_image_scanning - if scanning_enabled is True: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif scanning_enabled is True: report.status = "PASS" report.status_extended = ( f"Scanning of linked images for malicious content is enabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json index 0a8214370e..fdac312fce 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json @@ -38,5 +38,5 @@ "gmail_inbound_domain_spoofing_protection_enabled", "gmail_unauthenticated_email_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py index c4fb628e83..b76201c808 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py @@ -41,7 +41,13 @@ class gmail_groups_spoofing_protection_enabled(Check): else "all groups" ) - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection of groups from inbound emails spoofing your " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json index fef0f77322..aab6af3f40 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json @@ -38,5 +38,5 @@ "gmail_unauthenticated_email_protection_enabled", "gmail_groups_spoofing_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py index 822c445595..d40b8d4b3b 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py @@ -34,7 +34,13 @@ class gmail_inbound_domain_spoofing_protection_enabled(Check): consequence = gmail_client.policies.inbound_domain_spoofing_consequence domain = gmail_client.provider.identity.domain - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against inbound emails spoofing your domain " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.metadata.json index a4f9fd0460..7d5b4aa16b 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.metadata.json @@ -33,5 +33,5 @@ ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py index 5e32e58b99..493c0ec43e 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py @@ -26,7 +26,13 @@ class gmail_mail_delegation_disabled(Check): delegation_enabled = gmail_client.policies.enable_mail_delegation - if delegation_enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no mailbox can be delegated." + ) + elif delegation_enabled is False: report.status = "PASS" report.status_extended = ( f"Mail delegation is disabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.metadata.json index 95cd7567c3..679f321114 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.metadata.json @@ -36,5 +36,5 @@ "gmail_pop_imap_access_disabled", "gmail_auto_forwarding_disabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py index 3f384de1d0..8e4f05491b 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py @@ -26,7 +26,13 @@ class gmail_per_user_outbound_gateway_disabled(Check): gateway_allowed = gmail_client.policies.allow_per_user_outbound_gateway - if gateway_allowed is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no outbound gateway can be used." + ) + elif gateway_allowed is False: report.status = "PASS" report.status_extended = ( f"Per-user outbound gateways are disabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.metadata.json index 48f077d8ea..af52b71b15 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.metadata.json @@ -36,5 +36,5 @@ "gmail_auto_forwarding_disabled", "gmail_per_user_outbound_gateway_disabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py index 18b5034e31..e4ffa1baa7 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py @@ -27,7 +27,13 @@ class gmail_pop_imap_access_disabled(Check): pop_enabled = gmail_client.policies.enable_pop_access imap_enabled = gmail_client.policies.enable_imap_access - if pop_enabled is False and imap_enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no mailbox can be reached over POP or IMAP." + ) + elif pop_enabled is False and imap_enabled is False: report.status = "PASS" report.status_extended = ( f"POP and IMAP access are both disabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.metadata.json index cae7d474ac..8f41f0e38c 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.metadata.json @@ -36,5 +36,5 @@ "gmail_encrypted_attachment_protection_enabled", "gmail_anomalous_attachment_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.py index 1dd2ed59d1..3a5baff2f7 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.py @@ -27,7 +27,13 @@ class gmail_script_attachment_protection_enabled(Check): enabled = gmail_client.policies.enable_script_attachment_protection consequence = gmail_client.policies.script_attachment_protection_consequence - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against attachments with scripts from " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_service.py b/prowler/providers/googleworkspace/services/gmail/gmail_service.py index ed9dc71803..4354255ff4 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_service.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_service.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel - from prowler.lib.logger import logger -from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService +from prowler.providers.googleworkspace.lib.service.service import ( + GoogleWorkspaceService, + ServicePolicies, +) class Gmail(GoogleWorkspaceService): @@ -30,160 +31,21 @@ class Gmail(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("gmail.*")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("gmail.*")', + "fetching Gmail policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + ( + self.policies.service_state, + self.policies.service_state_overridden, + ) = self._service_status(policies, "gmail.service_status") - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - logger.debug(f"Processing setting type: {setting_type}") - - value = setting.get("value", {}) - - if setting_type == "gmail.mail_delegation": - self.policies.enable_mail_delegation = value.get( - "enableMailDelegation" - ) - logger.debug("Gmail mail delegation setting fetched.") - - elif setting_type == "gmail.email_attachment_safety": - self.policies.enable_encrypted_attachment_protection = ( - value.get("enableEncryptedAttachmentProtection") - ) - self.policies.encrypted_attachment_protection_consequence = value.get( - "encryptedAttachmentProtectionConsequence" - ) - self.policies.enable_script_attachment_protection = ( - value.get("enableAttachmentWithScriptsProtection") - ) - self.policies.script_attachment_protection_consequence = ( - value.get("scriptAttachmentProtectionConsequence") - ) - self.policies.enable_anomalous_attachment_protection = ( - value.get("enableAnomalousAttachmentProtection") - ) - self.policies.anomalous_attachment_protection_consequence = value.get( - "anomalousAttachmentProtectionConsequence" - ) - logger.debug("Gmail attachment safety settings fetched.") - - elif setting_type == "gmail.links_and_external_images": - self.policies.enable_shortener_scanning = value.get( - "enableShortenerScanning" - ) - self.policies.enable_external_image_scanning = value.get( - "enableExternalImageScanning" - ) - self.policies.enable_aggressive_warnings_on_untrusted_links = value.get( - "enableAggressiveWarningsOnUntrustedLinks" - ) - logger.debug( - "Gmail links and external images settings fetched." - ) - - elif setting_type == "gmail.spoofing_and_authentication": - self.policies.detect_domain_name_spoofing = value.get( - "detectDomainNameSpoofing" - ) - self.policies.domain_spoofing_consequence = value.get( - "domainSpoofingConsequence" - ) - self.policies.detect_employee_name_spoofing = value.get( - "detectEmployeeNameSpoofing" - ) - self.policies.employee_name_spoofing_consequence = ( - value.get("employeeNameSpoofingConsequence") - ) - self.policies.detect_inbound_domain_spoofing = value.get( - "detectDomainSpoofingFromUnauthenticatedSenders" - ) - self.policies.inbound_domain_spoofing_consequence = ( - value.get("inboundDomainSpoofingConsequence") - ) - self.policies.detect_unauthenticated_emails = value.get( - "detectUnauthenticatedEmails" - ) - self.policies.unauthenticated_email_consequence = value.get( - "unauthenticatedEmailConsequence" - ) - self.policies.detect_groups_spoofing = value.get( - "detectGroupsSpoofing" - ) - self.policies.groups_spoofing_visibility_type = value.get( - "groupsSpoofingVisibilityType" - ) - self.policies.groups_spoofing_consequence = value.get( - "groupsSpoofingConsequence" - ) - logger.debug( - "Gmail spoofing and authentication settings fetched." - ) - - elif setting_type == "gmail.pop_access": - self.policies.enable_pop_access = value.get( - "enablePopAccess" - ) - logger.debug("Gmail POP access setting fetched.") - - elif setting_type == "gmail.imap_access": - self.policies.enable_imap_access = value.get( - "enableImapAccess" - ) - logger.debug("Gmail IMAP access setting fetched.") - - elif setting_type == "gmail.auto_forwarding": - self.policies.enable_auto_forwarding = value.get( - "enableAutoForwarding" - ) - logger.debug("Gmail auto-forwarding setting fetched.") - - elif setting_type == "gmail.per_user_outbound_gateway": - self.policies.allow_per_user_outbound_gateway = value.get( - "allowUsersToUseExternalSmtpServers" - ) - logger.debug( - "Gmail per-user outbound gateway setting fetched." - ) - - elif ( - setting_type - == "gmail.enhanced_pre_delivery_message_scanning" - ): - self.policies.enable_enhanced_pre_delivery_scanning = ( - value.get("enableImprovedSuspiciousContentDetection") - ) - logger.debug( - "Gmail enhanced pre-delivery scanning setting fetched." - ) - - elif setting_type == "gmail.comprehensive_mail_storage": - self.policies.comprehensive_mail_storage_enabled = ( - value.get("ruleId") is not None - ) - logger.debug( - "Gmail comprehensive mail storage setting fetched." - ) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching Gmail policies", - self.provider.identity.customer_id, - ) - fetch_succeeded = False - break + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + self._process_setting(setting_type, setting.get("value", {})) self.policies_fetched = fetch_succeeded logger.info("Gmail policies fetched successfully.") @@ -196,8 +58,113 @@ class Gmail(GoogleWorkspaceService): ) self.policies_fetched = False + def _process_setting(self, setting_type: str, value: dict): + """Populate the model from a single policy setting.""" + logger.debug(f"Processing setting type: {setting_type}") -class GmailPolicies(BaseModel): + if setting_type == "gmail.mail_delegation": + self.policies.enable_mail_delegation = value.get("enableMailDelegation") + logger.debug("Gmail mail delegation setting fetched.") + + elif setting_type == "gmail.email_attachment_safety": + self.policies.enable_encrypted_attachment_protection = value.get( + "enableEncryptedAttachmentProtection" + ) + self.policies.encrypted_attachment_protection_consequence = value.get( + "encryptedAttachmentProtectionConsequence" + ) + self.policies.enable_script_attachment_protection = value.get( + "enableAttachmentWithScriptsProtection" + ) + self.policies.script_attachment_protection_consequence = value.get( + "scriptAttachmentProtectionConsequence" + ) + self.policies.enable_anomalous_attachment_protection = value.get( + "enableAnomalousAttachmentProtection" + ) + self.policies.anomalous_attachment_protection_consequence = value.get( + "anomalousAttachmentProtectionConsequence" + ) + logger.debug("Gmail attachment safety settings fetched.") + + elif setting_type == "gmail.links_and_external_images": + self.policies.enable_shortener_scanning = value.get( + "enableShortenerScanning" + ) + self.policies.enable_external_image_scanning = value.get( + "enableExternalImageScanning" + ) + self.policies.enable_aggressive_warnings_on_untrusted_links = value.get( + "enableAggressiveWarningsOnUntrustedLinks" + ) + logger.debug("Gmail links and external images settings fetched.") + + elif setting_type == "gmail.spoofing_and_authentication": + self.policies.detect_domain_name_spoofing = value.get( + "detectDomainNameSpoofing" + ) + self.policies.domain_spoofing_consequence = value.get( + "domainSpoofingConsequence" + ) + self.policies.detect_employee_name_spoofing = value.get( + "detectEmployeeNameSpoofing" + ) + self.policies.employee_name_spoofing_consequence = value.get( + "employeeNameSpoofingConsequence" + ) + self.policies.detect_inbound_domain_spoofing = value.get( + "detectDomainSpoofingFromUnauthenticatedSenders" + ) + self.policies.inbound_domain_spoofing_consequence = value.get( + "inboundDomainSpoofingConsequence" + ) + self.policies.detect_unauthenticated_emails = value.get( + "detectUnauthenticatedEmails" + ) + self.policies.unauthenticated_email_consequence = value.get( + "unauthenticatedEmailConsequence" + ) + self.policies.detect_groups_spoofing = value.get("detectGroupsSpoofing") + self.policies.groups_spoofing_visibility_type = value.get( + "groupsSpoofingVisibilityType" + ) + self.policies.groups_spoofing_consequence = value.get( + "groupsSpoofingConsequence" + ) + logger.debug("Gmail spoofing and authentication settings fetched.") + + elif setting_type == "gmail.pop_access": + self.policies.enable_pop_access = value.get("enablePopAccess") + logger.debug("Gmail POP access setting fetched.") + + elif setting_type == "gmail.imap_access": + self.policies.enable_imap_access = value.get("enableImapAccess") + logger.debug("Gmail IMAP access setting fetched.") + + elif setting_type == "gmail.auto_forwarding": + self.policies.enable_auto_forwarding = value.get("enableAutoForwarding") + logger.debug("Gmail auto-forwarding setting fetched.") + + elif setting_type == "gmail.per_user_outbound_gateway": + self.policies.allow_per_user_outbound_gateway = value.get( + "allowUsersToUseExternalSmtpServers" + ) + logger.debug("Gmail per-user outbound gateway setting fetched.") + + elif setting_type == "gmail.enhanced_pre_delivery_message_scanning": + self.policies.enable_enhanced_pre_delivery_scanning = value.get( + "enableImprovedSuspiciousContentDetection" + ) + logger.debug("Gmail enhanced pre-delivery scanning setting fetched.") + + elif setting_type == "gmail.comprehensive_mail_storage": + self.policies.comprehensive_mail_storage_enabled = ( + value.get("ruleId") is not None + ) + logger.debug("Gmail comprehensive mail storage setting fetched.") + + +class GmailPolicies(ServicePolicies): """Model for domain-level Gmail policy settings.""" # gmail.mail_delegation diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.metadata.json index 518b90b962..1a9e2e2106 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.metadata.json @@ -36,5 +36,5 @@ "gmail_external_image_scanning_enabled", "gmail_untrusted_link_warnings_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py index 01411dcc04..a6a1d681ff 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py @@ -26,7 +26,13 @@ class gmail_shortener_scanning_enabled(Check): scanning_enabled = gmail_client.policies.enable_shortener_scanning - if scanning_enabled is True: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif scanning_enabled is True: report.status = "PASS" report.status_extended = ( f"Identification of links behind shortened URLs is enabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.metadata.json index ec730d0a8b..744625a7cf 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.metadata.json @@ -38,5 +38,5 @@ "gmail_inbound_domain_spoofing_protection_enabled", "gmail_groups_spoofing_protection_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.py index 44ff6fa24d..f663a272c0 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.py @@ -27,7 +27,13 @@ class gmail_unauthenticated_email_protection_enabled(Check): enabled = gmail_client.policies.detect_unauthenticated_emails consequence = gmail_client.policies.unauthenticated_email_consequence - if enabled is False: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against unauthenticated emails is disabled " diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.metadata.json index 78678ff9f5..3456889c2f 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.metadata.json @@ -36,5 +36,5 @@ "gmail_shortener_scanning_enabled", "gmail_external_image_scanning_enabled" ], - "Notes": "" + "Notes": "When Gmail is turned off for everyone, the check passes: the setting has no effect while the service is off." } diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py index a62480de3b..531c0cc473 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py @@ -28,7 +28,13 @@ class gmail_untrusted_link_warnings_enabled(Check): gmail_client.policies.enable_aggressive_warnings_on_untrusted_links ) - if warnings_enabled is True: + if gmail_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Gmail is turned off for everyone in domain " + f"{gmail_client.provider.identity.domain}, so no user mailbox receives mail through Gmail." + ) + elif warnings_enabled is True: report.status = "PASS" report.status_extended = ( f"Warning prompts for clicks on untrusted domain links are enabled " diff --git a/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.metadata.json b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.metadata.json index 6a5bb80c3e..2691cecb55 100644 --- a/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.metadata.json +++ b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.metadata.json @@ -36,5 +36,5 @@ "groups_external_access_restricted", "groups_view_conversations_restricted" ], - "Notes": "" + "Notes": "When Groups for Business is turned off for everyone, only the group creation setting is exempt: no group can be created in the Google Groups app, but existing groups stay active, so the external members and incoming email settings are still evaluated." } diff --git a/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.py b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.py index ad9ce534ed..e6c4baf444 100644 --- a/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.py +++ b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.py @@ -13,6 +13,10 @@ class groups_creation_restricted(Check): - Only organization admins can create groups (not all users) - Group owners cannot allow external members - Group owners cannot allow incoming email from outside the organization + + When Groups for Business is off for everyone, nobody can create a group in + the Google Groups app, so the creation setting is moot. Existing groups + stay active and keep receiving mail, so the other two settings still count. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -35,10 +39,12 @@ class groups_creation_restricted(Check): incoming_mail = policies.owners_can_allow_incoming_mail_from_public issues = [] + service_off = policies.off_for_everyone # Check creation access level - # Default is USERS_IN_DOMAIN (insecure) — only ADMIN_ONLY is compliant - if access_level is None or access_level != "ADMIN_ONLY": + # Default is USERS_IN_DOMAIN (insecure) — only ADMIN_ONLY is compliant. + # Moot while the service is off: nobody can create a group then. + if not service_off and access_level != "ADMIN_ONLY": effective = access_level or "USERS_IN_DOMAIN (default)" issues.append( f"group creation is set to {effective} instead of ADMIN_ONLY" @@ -56,13 +62,28 @@ class groups_creation_restricted(Check): "group owners can allow incoming email from outside the organization" ) - if not issues: + if not issues and service_off: + report.status = "PASS" + report.status_extended = ( + f"Groups for Business is turned off for everyone in domain " + f"{domain}, so no group can be created, and existing groups " + f"allow no external members and no incoming email from outside." + ) + elif not issues: report.status = "PASS" report.status_extended = ( f"Group creation is properly restricted in domain {domain}: " f"admin-only creation, no external members, " f"no incoming email from outside." ) + elif service_off: + report.status = "FAIL" + issues_text = "; ".join(issues) + report.status_extended = ( + f"Groups for Business is turned off for everyone in domain " + f"{domain}, so no group can be created, but existing groups " + f"remain active and are not fully restricted: {issues_text}." + ) else: report.status = "FAIL" issues_text = "; ".join(issues) diff --git a/prowler/providers/googleworkspace/services/groups/groups_service.py b/prowler/providers/googleworkspace/services/groups/groups_service.py index 1ebc0508e3..f9b7204983 100644 --- a/prowler/providers/googleworkspace/services/groups/groups_service.py +++ b/prowler/providers/googleworkspace/services/groups/groups_service.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel - from prowler.lib.logger import logger -from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService +from prowler.providers.googleworkspace.lib.service.service import ( + GoogleWorkspaceService, + ServicePolicies, +) class Groups(GoogleWorkspaceService): @@ -30,62 +31,21 @@ class Groups(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("groups_for_business.*")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("groups_for_business.*")', + "fetching Groups for Business policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + ( + self.policies.service_state, + self.policies.service_state_overridden, + ) = self._service_status(policies, "groups_for_business.service_status") - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - logger.debug(f"Processing setting type: {setting_type}") - - value = setting.get("value", {}) - - if setting_type == "groups_for_business.groups_sharing": - self.policies.collaboration_capability = value.get( - "collaborationCapability" - ) - self.policies.create_groups_access_level = value.get( - "createGroupsAccessLevel" - ) - self.policies.owners_can_allow_external_members = value.get( - "ownersCanAllowExternalMembers" - ) - self.policies.owners_can_allow_incoming_mail_from_public = ( - value.get("ownersCanAllowIncomingMailFromPublic") - ) - self.policies.view_topics_default_access_level = value.get( - "viewTopicsDefaultAccessLevel" - ) - self.policies.owners_can_hide_groups = value.get( - "ownersCanHideGroups" - ) - self.policies.new_groups_are_hidden = value.get( - "newGroupsAreHidden" - ) - logger.debug( - "Groups for Business sharing settings fetched." - ) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching Groups for Business policies", - self.provider.identity.customer_id, - ) - fetch_succeeded = False - break + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + self._process_setting(setting_type, setting.get("value", {})) self.policies_fetched = fetch_succeeded @@ -104,8 +64,32 @@ class Groups(GoogleWorkspaceService): ) self.policies_fetched = False + def _process_setting(self, setting_type: str, value: dict): + """Populate the model from a single policy setting.""" + logger.debug(f"Processing setting type: {setting_type}") -class GroupsPolicies(BaseModel): + if setting_type == "groups_for_business.groups_sharing": + self.policies.collaboration_capability = value.get( + "collaborationCapability" + ) + self.policies.create_groups_access_level = value.get( + "createGroupsAccessLevel" + ) + self.policies.owners_can_allow_external_members = value.get( + "ownersCanAllowExternalMembers" + ) + self.policies.owners_can_allow_incoming_mail_from_public = value.get( + "ownersCanAllowIncomingMailFromPublic" + ) + self.policies.view_topics_default_access_level = value.get( + "viewTopicsDefaultAccessLevel" + ) + self.policies.owners_can_hide_groups = value.get("ownersCanHideGroups") + self.policies.new_groups_are_hidden = value.get("newGroupsAreHidden") + logger.debug("Groups for Business sharing settings fetched.") + + +class GroupsPolicies(ServicePolicies): """Model for domain-level Groups for Business policy settings.""" # groups_for_business.groups_sharing diff --git a/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.metadata.json b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.metadata.json index 88f0927155..8b3ee086cd 100644 --- a/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.metadata.json +++ b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.metadata.json @@ -36,5 +36,5 @@ "groups_external_access_restricted", "groups_creation_restricted" ], - "Notes": "" + "Notes": "When Groups for Business is turned off for everyone, the check passes: the setting only governs who can read conversations in the Google Groups app, which is off. Existing groups keep delivering mail to their members." } diff --git a/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.py b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.py index 66fb15a43b..6a9fc9b5a3 100644 --- a/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.py +++ b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.py @@ -29,7 +29,13 @@ class groups_view_conversations_restricted(Check): view_access = groups_client.policies.view_topics_default_access_level domain = groups_client.provider.identity.domain - if view_access == "GROUP_MEMBERS": + if groups_client.policies.off_for_everyone: + report.status = "PASS" + report.status_extended = ( + f"Groups for Business is turned off for everyone in domain " + f"{groups_client.provider.identity.domain}, so no group conversation can be viewed in the Google Groups app." + ) + elif view_access == "GROUP_MEMBERS": report.status = "PASS" report.status_extended = ( f"Default permission to view conversations is set to " diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py index 4c117a4468..5aee87785f 100644 --- a/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py @@ -30,41 +30,22 @@ class Marketplace(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("workspace_marketplace.*")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("workspace_marketplace.*")', + "fetching Marketplace policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - value = setting.get("value", {}) - - if setting_type == "workspace_marketplace.apps_access_options": - self.policies.access_level = value.get("accessLevel") - logger.debug( - "Marketplace access level: " - f"{self.policies.access_level}" - ) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching Marketplace policies", - self.provider.identity.customer_id, + if setting_type == "workspace_marketplace.apps_access_options": + self.policies.access_level = value.get("accessLevel") + logger.debug( + "Marketplace access level: " f"{self.policies.access_level}" ) - fetch_succeeded = False - break self.policies_fetched = fetch_succeeded diff --git a/prowler/providers/googleworkspace/services/security/security_service.py b/prowler/providers/googleworkspace/services/security/security_service.py index dd0cbff557..acb7173ab3 100644 --- a/prowler/providers/googleworkspace/services/security/security_service.py +++ b/prowler/providers/googleworkspace/services/security/security_service.py @@ -78,52 +78,33 @@ class Security(GoogleWorkspaceService): def _fetch_namespace(self, service, filter_str: str, fetch_succeeded: bool) -> bool: """Fetch policies for a single namespace filter.""" - try: - request = service.policies().list( - pageSize=100, - filter=filter_str, + policies, succeeded = self._list_policies( + service, filter_str, f"fetching policies with filter {filter_str}" + ) + + customer_level = [] + for policy in policies: + setting_type = ( + policy.get("setting", {}).get("type", "").removeprefix("settings/") ) - while request is not None: - try: - response = request.execute() + scope = self._policy_scope(policy) + if scope != CUSTOMER_SCOPE: + if scope == UNKNOWN_SCOPE: + self.policies.unresolved_scope = True + elif setting_type: + self._overridden.add(setting_type) + continue - for policy in response.get("policies", []): - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") + self._observed.add(setting_type) + customer_level.append(policy) - scope = self._policy_scope(policy) - if scope != CUSTOMER_SCOPE: - if scope == UNKNOWN_SCOPE: - self.policies.unresolved_scope = True - elif setting_type: - self._overridden.add(setting_type) - continue + for policy in self._effective_policies(customer_level): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + self._process_setting(setting_type, setting.get("value", {})) - self._observed.add(setting_type) - value = setting.get("value", {}) - - self._process_setting(setting_type, value) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - f"fetching policies with filter {filter_str}", - self.provider.identity.customer_id, - ) - return False - - except Exception as error: - self._handle_api_error( - error, - f"listing policies with filter {filter_str}", - self.provider.identity.customer_id, - ) - return False - - return fetch_succeeded + return fetch_succeeded and succeeded def _process_setting(self, setting_type: str, value: dict): """Process a single policy setting and populate the model.""" diff --git a/prowler/providers/googleworkspace/services/sites/sites_service.py b/prowler/providers/googleworkspace/services/sites/sites_service.py index 456c2aca65..543c67e38d 100644 --- a/prowler/providers/googleworkspace/services/sites/sites_service.py +++ b/prowler/providers/googleworkspace/services/sites/sites_service.py @@ -30,40 +30,22 @@ class Sites(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list( - pageSize=100, - filter='setting.type.matches("sites.*")', + policies, fetch_succeeded = self._list_policies( + service, + 'setting.type.matches("sites.*")', + "fetching Sites policies", ) - fetch_succeeded = True - while request is not None: - try: - response = request.execute() + for policy in self._effective_policies(policies): + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) - for policy in response.get("policies", []): - if not self._is_customer_level_policy(policy): - continue - - setting = policy.get("setting", {}) - setting_type = setting.get("type", "").removeprefix("settings/") - value = setting.get("value", {}) - - if setting_type == "sites.service_status": - self.policies.service_state = value.get("serviceState") - logger.debug( - "Sites service state: " f"{self.policies.service_state}" - ) - - request = service.policies().list_next(request, response) - - except Exception as error: - self._handle_api_error( - error, - "fetching Sites policies", - self.provider.identity.customer_id, + if setting_type == "sites.service_status": + self.policies.service_state = value.get("serviceState") + logger.debug( + "Sites service state: " f"{self.policies.service_state}" ) - fetch_succeeded = False - break self.policies_fetched = fetch_succeeded diff --git a/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py b/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py index 5e72d285d8..45053fba56 100644 --- a/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py +++ b/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py @@ -7,6 +7,7 @@ from prowler.providers.googleworkspace.lib.service.service import ( OVERRIDE_SCOPE, UNKNOWN_SCOPE, GoogleWorkspaceService, + ServicePolicies, ) ROOT_OU_ID = "03ph8a2z1234" @@ -125,3 +126,236 @@ class TestPolicyScope: svc._policy_scope({"policyQuery": {"group": "groups/xyz"}}) == OVERRIDE_SCOPE ) + + +def _policy( + setting_type, + value, + sort_order=None, + org_unit=f"orgUnits/{ROOT_OU_ID}", + group=None, +): + """Build a policy as the Cloud Identity Policy API returns it.""" + policy = {"setting": {"type": f"settings/{setting_type}", "value": value}} + query = {} + if org_unit: + query["orgUnit"] = org_unit + if group: + query["group"] = group + if sort_order is not None: + query["sortOrder"] = sort_order + if query: + policy["policyQuery"] = query + return policy + + +class TestEffectivePolicies: + def test_the_greatest_sort_order_wins_whatever_the_api_order(self): + """The ADMIN policy saved in the console outranks Google's SYSTEM defaults, even when the API returns it first""" + admin = _policy("security.password", {"minimumLength": 12}, 201.00131) + default_a = _policy("security.password", {"minimumLength": 8}, 100.00001) + default_b = _policy("security.password", {"minimumLength": 8}, 100.00003) + svc = _make_service() + + assert svc._effective_policies([admin, default_a, default_b]) == [admin] + assert svc._effective_policies([default_a, default_b, admin]) == [admin] + + def test_each_setting_type_is_reduced_on_its_own(self): + password_admin = _policy("security.password", {"minimumLength": 12}, 201.1) + password_default = _policy("security.password", {"minimumLength": 8}, 100.1) + session_default = _policy( + "security.session_controls", {"webSessionDuration": "1209600s"}, 100.3 + ) + + assert _make_service()._effective_policies( + [password_admin, password_default, session_default] + ) == [password_admin, session_default] + + def test_a_single_policy_is_kept_as_is(self): + policy = _policy("sites.service_status", {"serviceState": "ENABLED"}) + + assert _make_service()._effective_policies([policy]) == [policy] + + def test_without_a_sort_order_the_last_policy_wins(self): + """No sort order means no documented precedence, so the API order is kept""" + first = _policy("sites.service_status", {"serviceState": "ENABLED"}) + last = _policy("sites.service_status", {"serviceState": "DISABLED"}) + + assert _make_service()._effective_policies([first, last]) == [last] + + def test_policies_outside_the_customer_scope_are_dropped(self): + root = _policy("security.password", {"minimumLength": 8}, 100.1) + sub_ou = _policy( + "security.password", {"minimumLength": 14}, 300.1, org_unit="orgUnits/sub" + ) + group = _policy( + "security.password", {"minimumLength": 16}, 400.1, group="groups/g1" + ) + + assert _make_service()._effective_policies([sub_ou, root, group]) == [root] + + def test_rules_are_items_rather_than_values_so_every_one_is_kept(self): + """Each rule.* policy is a distinct rule, not a competing value for one setting""" + dlp_a = _policy("rule.dlp", {"displayName": "cards", "state": "ACTIVE"}, 1) + dlp_b = _policy("rule.dlp", {"displayName": "ids", "state": "INACTIVE"}, 2) + alert = _policy("rule.system_defined_alerts", {"displayName": "Leaked"}, 1) + + assert _make_service()._effective_policies([dlp_a, dlp_b, alert]) == [ + dlp_a, + dlp_b, + alert, + ] + + def test_no_policies(self): + assert _make_service()._effective_policies([]) == [] + + +class TestServiceStatus: + def test_off_at_the_customer_level_with_no_override(self): + status = _policy("chat.service_status", {"serviceState": "DISABLED"}, 201.1) + other = _policy("chat.chat_apps_access", {"enableApps": True}, 100.1) + + assert _make_service()._service_status( + [other, status], "chat.service_status" + ) == ( + "DISABLED", + False, + ) + + def test_the_greatest_sort_order_gives_the_state(self): + default = _policy("chat.service_status", {"serviceState": "ENABLED"}, 100.1) + admin = _policy("chat.service_status", {"serviceState": "DISABLED"}, 201.1) + + assert _make_service()._service_status( + [admin, default], "chat.service_status" + ) == ( + "DISABLED", + False, + ) + + def test_an_org_unit_or_group_with_its_own_status_is_an_override(self): + root = _policy("chat.service_status", {"serviceState": "DISABLED"}, 201.1) + sub_ou = _policy( + "chat.service_status", + {"serviceState": "ENABLED"}, + 301.1, + org_unit="orgUnits/sub", + ) + group = _policy( + "chat.service_status", {"serviceState": "ENABLED"}, 401.1, group="groups/g1" + ) + + svc = _make_service() + assert svc._service_status([root, sub_ou], "chat.service_status") == ( + "DISABLED", + True, + ) + assert svc._service_status([root, group], "chat.service_status") == ( + "DISABLED", + True, + ) + + def test_a_sub_org_unit_that_is_also_off_is_not_an_override(self): + """An org unit that turns the service off as well changes nothing for anyone""" + root = _policy("chat.service_status", {"serviceState": "DISABLED"}, 201.1) + sub_ou = _policy( + "chat.service_status", + {"serviceState": "DISABLED"}, + 301.1, + org_unit="orgUnits/sub", + ) + + assert _make_service()._service_status( + [root, sub_ou], "chat.service_status" + ) == ( + "DISABLED", + False, + ) + + def test_an_unresolved_scope_counts_as_an_override(self): + """Without the root OU id a scoped policy may be the root or a sub-OU; neither may be assumed""" + status = _policy("chat.service_status", {"serviceState": "DISABLED"}, 201.1) + + assert _make_service(root_org_unit_id=None)._service_status( + [status], "chat.service_status" + ) == (None, True) + + def test_no_status_policy(self): + other = _policy("chat.chat_apps_access", {"enableApps": True}, 100.1) + + assert _make_service()._service_status([other], "chat.service_status") == ( + None, + False, + ) + + +class TestServicePolicies: + @pytest.mark.parametrize( + "state, overridden, expected", + [ + ("DISABLED", False, True), + ("DISABLED", True, False), + ("ENABLED", False, False), + (None, False, False), + ], + ) + def test_off_for_everyone(self, state, overridden, expected): + policies = ServicePolicies( + service_state=state, service_state_overridden=overridden + ) + + assert policies.off_for_everyone is expected + + +class TestListPolicies: + def test_collects_every_page(self): + svc = _make_service() + page_1 = {"policies": [_policy("gmail.pop_access", {"enablePopAccess": True})]} + page_2 = { + "policies": [_policy("gmail.imap_access", {"enableImapAccess": True})] + } + request_1, request_2 = MagicMock(), MagicMock() + request_1.execute.return_value = page_1 + request_2.execute.return_value = page_2 + service = MagicMock() + service.policies().list.return_value = request_1 + service.policies().list_next.side_effect = [request_2, None] + + policies, succeeded = svc._list_policies( + service, 'setting.type.matches("gmail.*")', "fetching Gmail policies" + ) + + assert succeeded is True + assert policies == page_1["policies"] + page_2["policies"] + service.policies().list.assert_called_once_with( + pageSize=100, filter='setting.type.matches("gmail.*")' + ) + + def test_a_failing_page_keeps_what_was_read_and_reports_the_failure(self): + svc = _make_service() + page_1 = {"policies": [_policy("gmail.pop_access", {"enablePopAccess": True})]} + request_1, request_2 = MagicMock(), MagicMock() + request_1.execute.return_value = page_1 + request_2.execute.side_effect = Exception("boom") + service = MagicMock() + service.policies().list.return_value = request_1 + service.policies().list_next.side_effect = [request_2, None] + + policies, succeeded = svc._list_policies( + service, 'setting.type.matches("gmail.*")', "fetching Gmail policies" + ) + + assert succeeded is False + assert policies == page_1["policies"] + + def test_a_failing_list_call_reports_the_failure(self): + svc = _make_service() + service = MagicMock() + service.policies().list.side_effect = Exception("boom") + + policies, succeeded = svc._list_policies( + service, 'setting.type.matches("gmail.*")', "fetching Gmail policies" + ) + + assert succeeded is False + assert policies == [] diff --git a/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py b/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py index 85d9afea45..15e7a7fee5 100644 --- a/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py +++ b/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py @@ -97,6 +97,59 @@ class TestAdditionalServicesService: assert additional_services.policies_fetched is True assert additional_services.policies.groups_service_state == "ENABLED" + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/groups.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.00002}, + "setting": { + "type": "settings/groups.service_status", + "value": {"serviceState": "ENABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is True + assert additional_services.policies.groups_service_state == "DISABLED" + def test_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py index 5ff2cee8f6..e59b099cd3 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py @@ -5,6 +5,7 @@ from prowler.providers.googleworkspace.services.calendar.calendar_service import ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -102,6 +103,38 @@ class TestCalendarExternalInvitationsWarning: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning.calendar_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning import ( + calendar_external_invitations_warning, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = CalendarPolicies( + service_state="DISABLED", external_invitations_warning=False + ) + + findings = calendar_external_invitations_warning().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Calendar is turned off for everyone in domain {DOMAIN}, " + f"so no invitation can be sent." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py index 175955f4b6..d9904fa3cb 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py @@ -5,6 +5,7 @@ from prowler.providers.googleworkspace.services.calendar.calendar_service import ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -140,6 +141,39 @@ class TestCalendarExternalSharingPrimaryCalendar: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar.calendar_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar import ( + calendar_external_sharing_primary_calendar, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = CalendarPolicies( + service_state="DISABLED", + primary_calendar_external_sharing="EXTERNAL_ALL_INFO_READ_WRITE", + ) + + findings = calendar_external_sharing_primary_calendar().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Calendar is turned off for everyone in domain {DOMAIN}, " + f"so no calendar can be shared outside the organization." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py index b513860125..85691ad607 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py @@ -5,6 +5,7 @@ from prowler.providers.googleworkspace.services.calendar.calendar_service import ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -133,6 +134,39 @@ class TestCalendarExternalSharingSecondaryCalendar: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar.calendar_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar import ( + calendar_external_sharing_secondary_calendar, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = CalendarPolicies( + service_state="DISABLED", + secondary_calendar_external_sharing="EXTERNAL_ALL_INFO_READ_WRITE", + ) + + findings = calendar_external_sharing_secondary_calendar().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Calendar is turned off for everyone in domain {DOMAIN}, " + f"so no calendar can be shared outside the organization." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/calendar/calendar_service_test.py b/tests/providers/googleworkspace/services/calendar/calendar_service_test.py index 760624e0ae..e908bc1388 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_service_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_service_test.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch from tests.providers.googleworkspace.googleworkspace_fixtures import ( + ROOT_ORG_UNIT_ID, set_mocked_googleworkspace_provider, ) @@ -77,6 +78,171 @@ class TestCalendarService: ) assert calendar.policies.external_invitations_warning is True + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/calendar.secondary_calendar_max_allowed_external_sharing", + "value": { + "maxAllowedExternalSharing": "EXTERNAL_FREE_BUSY_ONLY" + }, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.00004}, + "setting": { + "type": "settings/calendar.secondary_calendar_max_allowed_external_sharing", + "value": { + "maxAllowedExternalSharing": "EXTERNAL_ALL_INFO_READ_ONLY" + }, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.calendar.calendar_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.calendar.calendar_service import ( + Calendar, + ) + + calendar = Calendar(mock_provider) + + assert calendar.policies_fetched is True + assert ( + calendar.policies.secondary_calendar_external_sharing + == "EXTERNAL_FREE_BUSY_ONLY" + ) + + def test_off_for_everyone_when_off_at_the_root_with_no_override(self): + """A root-level DISABLED status with no org unit or group override is off for everyone""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": { + "orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}", + "sortOrder": 201.00122, + }, + "setting": { + "type": "settings/calendar.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.calendar.calendar_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.calendar.calendar_service import ( + Calendar, + ) + + fetched = Calendar(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is False + assert fetched.policies.off_for_everyone is True + + def test_reads_the_service_status_and_its_overrides(self): + """Off at the customer level only counts when no org unit or group turns the service back on""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00122}, + "setting": { + "type": "settings/calendar.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + { + "type": "ADMIN", + "policyQuery": {"orgUnit": "orgUnits/sub", "sortOrder": 301.00122}, + "setting": { + "type": "settings/calendar.service_status", + "value": {"serviceState": "ENABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.calendar.calendar_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.calendar.calendar_service import ( + Calendar, + ) + + fetched = Calendar(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is True + assert fetched.policies.off_for_everyone is False + def test_calendar_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled_test.py index a476321754..9bb7e35c47 100644 --- a/tests/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled_test.py +++ b/tests/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -93,6 +94,69 @@ class TestChatAppsInstallationDisabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import ( + chat_apps_installation_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + service_state="DISABLED", enable_apps=True + ) + + findings = chat_apps_installation_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Chat is turned off for everyone in domain {DOMAIN}, " + f"so Chat apps cannot be installed." + ) + + def test_evaluates_the_setting_when_an_override_turns_the_service_on(self): + """Off at the customer level with an org unit turned back on is not off for everyone""" + 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.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import ( + chat_apps_installation_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + service_state="DISABLED", + service_state_overridden=True, + enable_apps=True, + ) + + findings = chat_apps_installation_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled_test.py index 429375ebe5..37cae34c4d 100644 --- a/tests/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled_test.py +++ b/tests/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -123,6 +124,38 @@ class TestChatExternalFileSharingDisabled: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import ( + chat_external_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + service_state="DISABLED", external_file_sharing="ALL_FILES" + ) + + findings = chat_external_file_sharing_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Chat is turned off for everyone in domain {DOMAIN}, " + f"so no files can be shared externally through Chat." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted_test.py b/tests/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted_test.py index 09fa3d2dbc..9e3dd42907 100644 --- a/tests/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted_test.py +++ b/tests/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -128,6 +129,40 @@ class TestChatExternalMessagingRestricted: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import ( + chat_external_messaging_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + service_state="DISABLED", + allow_external_chat=True, + external_chat_restriction="NO_RESTRICTION", + ) + + findings = chat_external_messaging_restricted().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Chat is turned off for everyone in domain {DOMAIN}, " + f"so no external messaging can take place." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted_test.py b/tests/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted_test.py index 608ae11d01..1a4990ee35 100644 --- a/tests/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted_test.py +++ b/tests/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -129,6 +130,40 @@ class TestChatExternalSpacesRestricted: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import ( + chat_external_spaces_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + service_state="DISABLED", + external_spaces_enabled=True, + external_spaces_domain_allowlist_mode="ALL_DOMAINS", + ) + + findings = chat_external_spaces_restricted().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Chat is turned off for everyone in domain {DOMAIN}, " + f"so no external spaces can be created or joined." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled_test.py index 712bb21fa5..279ee57e9e 100644 --- a/tests/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled_test.py +++ b/tests/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -93,6 +94,38 @@ class TestChatIncomingWebhooksDisabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import ( + chat_incoming_webhooks_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + service_state="DISABLED", enable_webhooks=True + ) + + findings = chat_incoming_webhooks_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Chat is turned off for everyone in domain {DOMAIN}, " + f"so incoming webhooks cannot be used." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled_test.py index 297d00548d..f787cdd0f8 100644 --- a/tests/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled_test.py +++ b/tests/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -96,6 +97,38 @@ class TestChatInternalFileSharingDisabled: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import ( + chat_internal_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + service_state="DISABLED", internal_file_sharing="ALL_FILES" + ) + + findings = chat_internal_file_sharing_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Chat is turned off for everyone in domain {DOMAIN}, " + f"so no files can be shared through Chat." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/chat/chat_service_test.py b/tests/providers/googleworkspace/services/chat/chat_service_test.py index 673f1eb3cd..7b39368142 100644 --- a/tests/providers/googleworkspace/services/chat/chat_service_test.py +++ b/tests/providers/googleworkspace/services/chat/chat_service_test.py @@ -93,6 +93,165 @@ class TestChatService: assert chat.policies.enable_apps is False assert chat.policies.enable_webhooks is False + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/chat.chat_apps_access", + "value": {"enableApps": False, "enableWebhooks": False}, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.00005}, + "setting": { + "type": "settings/chat.chat_apps_access", + "value": {"enableApps": True, "enableWebhooks": True}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is True + assert chat.policies.enable_apps is False + assert chat.policies.enable_webhooks is False + + def test_off_for_everyone_when_off_at_the_root_with_no_override(self): + """A root-level DISABLED status with no org unit or group override is off for everyone""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": { + "orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}", + "sortOrder": 201.00122, + }, + "setting": { + "type": "settings/chat.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + fetched = Chat(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is False + assert fetched.policies.off_for_everyone is True + + def test_reads_the_service_status_and_its_overrides(self): + """Off at the customer level only counts when no org unit or group turns the service back on""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00122}, + "setting": { + "type": "settings/chat.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + { + "type": "ADMIN", + "policyQuery": {"orgUnit": "orgUnits/sub", "sortOrder": 301.00122}, + "setting": { + "type": "settings/chat.service_status", + "value": {"serviceState": "ENABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + fetched = Chat(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is True + assert fetched.policies.off_for_everyone is False + def test_chat_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py b/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py index e200836bcd..abc0af5d6a 100644 --- a/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -128,6 +129,39 @@ class TestDriveAccessCheckerRecipientsOnly: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only import ( + drive_access_checker_recipients_only, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", + access_checker_suggestions="RECIPIENTS_OR_AUDIENCE_OR_PUBLIC", + ) + + findings = drive_access_checker_recipients_only().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Drive is turned off for everyone in domain {DOMAIN}, " + f"so Access Checker cannot widen any share." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py b/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py index 7225974c68..ee88f5b4b4 100644 --- a/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -94,6 +95,38 @@ class TestDriveDesktopAccessDisabled: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.drive.drive_desktop_access_disabled.drive_desktop_access_disabled.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled import ( + drive_desktop_access_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", allow_drive_for_desktop=True + ) + + findings = drive_desktop_access_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Drive is turned off for everyone in domain {DOMAIN}, " + f"so Drive for desktop cannot sync any content." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py index 3b66837460..ccc84550ac 100644 --- a/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py @@ -99,6 +99,35 @@ class TestDriveExternalSharingWarnUsers: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users import ( + drive_external_sharing_warn_users, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", warn_for_external_sharing=False + ) + + findings = drive_external_sharing_warn_users().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py b/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py index b089d52d58..1aa1fcc224 100644 --- a/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py @@ -130,6 +130,36 @@ class TestDriveInternalUsersDistributeContent: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content import ( + drive_internal_users_distribute_content, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", + allowed_parties_for_distributing_content="ALL_ELIGIBLE_USERS", + ) + + findings = drive_internal_users_distribute_content().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py b/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py index 8d6a0efcff..652600f3ea 100644 --- a/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py @@ -94,6 +94,35 @@ class TestDrivePublishingFilesDisabled: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_publishing_files_disabled.drive_publishing_files_disabled.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled import ( + drive_publishing_files_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", allow_publishing_files=True + ) + + findings = drive_publishing_files_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_service_test.py b/tests/providers/googleworkspace/services/drive/drive_service_test.py index cbdd56679c..580c9479e1 100644 --- a/tests/providers/googleworkspace/services/drive/drive_service_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_service_test.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch from tests.providers.googleworkspace.googleworkspace_fixtures import ( + ROOT_ORG_UNIT_ID, set_mocked_googleworkspace_provider, ) @@ -92,6 +93,164 @@ class TestDriveService: ) assert drive.policies.allow_drive_for_desktop is False + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/drive_and_docs.external_sharing", + "value": {"externalSharingMode": "ALLOWLISTED_DOMAINS"}, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.0001}, + "setting": { + "type": "settings/drive_and_docs.external_sharing", + "value": {"externalSharingMode": "ALLOWED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + drive = Drive(mock_provider) + + assert drive.policies_fetched is True + assert drive.policies.external_sharing_mode == "ALLOWLISTED_DOMAINS" + + def test_off_for_everyone_when_off_at_the_root_with_no_override(self): + """A root-level DISABLED status with no org unit or group override is off for everyone""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": { + "orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}", + "sortOrder": 201.00122, + }, + "setting": { + "type": "settings/drive_and_docs.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + fetched = Drive(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is False + assert fetched.policies.off_for_everyone is True + + def test_reads_the_service_status_and_its_overrides(self): + """Off at the customer level only counts when no org unit or group turns the service back on""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00122}, + "setting": { + "type": "settings/drive_and_docs.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + { + "type": "ADMIN", + "policyQuery": {"orgUnit": "orgUnits/sub", "sortOrder": 301.00122}, + "setting": { + "type": "settings/drive_and_docs.service_status", + "value": {"serviceState": "ENABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + fetched = Drive(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is True + assert fetched.policies.off_for_everyone is False + def test_drive_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py index 6ad0222f06..1e67e7be3a 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -96,6 +97,38 @@ class TestDriveSharedDriveCreationAllowed: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed import ( + drive_shared_drive_creation_allowed, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", allow_shared_drive_creation=False + ) + + findings = drive_shared_drive_creation_allowed().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Google Drive is turned off for everyone in domain {DOMAIN}, " + f"so no shared drive can be created." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py index 4a7a926745..89e82297c4 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py @@ -130,6 +130,35 @@ class TestDriveSharedDriveDisableDownloadPrintCopy: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy import ( + drive_shared_drive_disable_download_print_copy, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", allowed_parties_for_download_print_copy="ALL" + ) + + findings = drive_shared_drive_disable_download_print_copy().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py index 99e4af7d16..c903db1b4b 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py @@ -100,6 +100,35 @@ class TestDriveSharedDriveManagersCannotOverride: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override import ( + drive_shared_drive_managers_cannot_override, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", allow_managers_to_override_settings=True + ) + + findings = drive_shared_drive_managers_cannot_override().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py index b25fa33cf0..1556875011 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py @@ -94,6 +94,35 @@ class TestDriveSharedDriveMembersOnlyAccess: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access import ( + drive_shared_drive_members_only_access, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", allow_non_member_access=True + ) + + findings = drive_shared_drive_members_only_access().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py b/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py index 337afdc9aa..7822eea7fb 100644 --- a/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py @@ -126,6 +126,35 @@ class TestDriveSharingAllowlistedDomains: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains import ( + drive_sharing_allowlisted_domains, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", external_sharing_mode="ALLOWED" + ) + + findings = drive_sharing_allowlisted_domains().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py index 1aadeb924b..471cc15fc9 100644 --- a/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py @@ -100,6 +100,36 @@ class TestDriveWarnSharingWithAllowlistedDomains: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing shares stay reachable when Drive is off, so the stored setting still counts""" + 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.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains.drive_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains import ( + drive_warn_sharing_with_allowlisted_domains, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = DrivePolicies( + service_state="DISABLED", + warn_for_sharing_outside_allowlisted_domains=False, + ) + + findings = drive_warn_sharing_with_allowlisted_domains().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled_test.py index a57bee1ff1..e15c727df2 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -127,6 +128,38 @@ class TestGmailAnomalousAttachmentProtectionEnabled: assert findings[0].status == "FAIL" assert "insecure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled import ( + gmail_anomalous_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_anomalous_attachment_protection=False + ) + + findings = gmail_anomalous_attachment_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py index 2de8a79b9b..e9f06afe0c 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -96,6 +97,38 @@ class TestGmailAutoForwardingDisabled: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_auto_forwarding_disabled.gmail_auto_forwarding_disabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_auto_forwarding_disabled.gmail_auto_forwarding_disabled import ( + gmail_auto_forwarding_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_auto_forwarding=True + ) + + findings = gmail_auto_forwarding_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no email can be forwarded." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py index 1dde307b71..132c3bbd24 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -97,6 +98,38 @@ class TestGmailComprehensiveMailStorageEnabled: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_comprehensive_mail_storage_enabled.gmail_comprehensive_mail_storage_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_comprehensive_mail_storage_enabled.gmail_comprehensive_mail_storage_enabled import ( + gmail_comprehensive_mail_storage_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", comprehensive_mail_storage_enabled=False + ) + + findings = gmail_comprehensive_mail_storage_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so there is no Gmail mail to store." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py index a87979ee5c..5c0d57d298 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -189,6 +190,38 @@ class TestGmailDomainSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "default action" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", detect_domain_name_spoofing=False + ) + + findings = gmail_domain_spoofing_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py index c4ba415ba7..36f9b770c8 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -157,6 +158,38 @@ class TestGmailEmployeeNameSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "default action" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled import ( + gmail_employee_name_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", detect_employee_name_spoofing=False + ) + + findings = gmail_employee_name_spoofing_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled_test.py index cc41ff3f66..da72d76b93 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -127,6 +128,38 @@ class TestGmailEncryptedAttachmentProtectionEnabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled import ( + gmail_encrypted_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_encrypted_attachment_protection=False + ) + + findings = gmail_encrypted_attachment_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py index 823f7ee899..caf1d59c53 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -97,6 +98,38 @@ class TestGmailEnhancedPreDeliveryScanningEnabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_enhanced_pre_delivery_scanning_enabled.gmail_enhanced_pre_delivery_scanning_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_enhanced_pre_delivery_scanning_enabled.gmail_enhanced_pre_delivery_scanning_enabled import ( + gmail_enhanced_pre_delivery_scanning_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_enhanced_pre_delivery_scanning=False + ) + + findings = gmail_enhanced_pre_delivery_scanning_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py index 17b0bf7624..01782be7b1 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -91,6 +92,38 @@ class TestGmailExternalImageScanningEnabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_external_image_scanning_enabled.gmail_external_image_scanning_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_external_image_scanning_enabled.gmail_external_image_scanning_enabled import ( + gmail_external_image_scanning_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_external_image_scanning=False + ) + + findings = gmail_external_image_scanning_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py index 1decd300f2..aab36d881c 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -190,6 +191,38 @@ class TestGmailGroupsSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "insecure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", detect_groups_spoofing=False + ) + + findings = gmail_groups_spoofing_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py index 1677df655c..9f008f3f82 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -157,6 +158,38 @@ class TestGmailInboundDomainSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "default action" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled import ( + gmail_inbound_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", detect_inbound_domain_spoofing=False + ) + + findings = gmail_inbound_domain_spoofing_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py index e471d030ff..e532f122b5 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -91,6 +92,38 @@ class TestGmailMailDelegationDisabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_mail_delegation_disabled.gmail_mail_delegation_disabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_mail_delegation_disabled.gmail_mail_delegation_disabled import ( + gmail_mail_delegation_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_mail_delegation=True + ) + + findings = gmail_mail_delegation_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no mailbox can be delegated." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py index 2fe105df42..66cd324714 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -91,6 +92,38 @@ class TestGmailPerUserOutboundGatewayDisabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_per_user_outbound_gateway_disabled.gmail_per_user_outbound_gateway_disabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_per_user_outbound_gateway_disabled.gmail_per_user_outbound_gateway_disabled import ( + gmail_per_user_outbound_gateway_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", allow_per_user_outbound_gateway=True + ) + + findings = gmail_per_user_outbound_gateway_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no outbound gateway can be used." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py index d5f87ac2c8..fb55ead078 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -156,6 +157,40 @@ class TestGmailPopImapAccessDisabled: assert findings[0].status == "FAIL" assert "not explicitly configured" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_pop_imap_access_disabled.gmail_pop_imap_access_disabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_pop_imap_access_disabled.gmail_pop_imap_access_disabled import ( + gmail_pop_imap_access_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", + enable_pop_access=True, + enable_imap_access=True, + ) + + findings = gmail_pop_imap_access_disabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no mailbox can be reached over POP or IMAP." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled_test.py index 9f04178457..5eaef5ccd1 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -127,6 +128,38 @@ class TestGmailScriptAttachmentProtectionEnabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled import ( + gmail_script_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_script_attachment_protection=False + ) + + findings = gmail_script_attachment_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_service_test.py b/tests/providers/googleworkspace/services/gmail/gmail_service_test.py index cdfa29ed14..6547b6f4fd 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_service_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_service_test.py @@ -162,6 +162,164 @@ class TestGmailService: assert gmail.policies.enable_enhanced_pre_delivery_scanning is True assert gmail.policies.comprehensive_mail_storage_enabled is True + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/gmail.auto_forwarding", + "value": {"enableAutoForwarding": False}, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.00008}, + "setting": { + "type": "settings/gmail.auto_forwarding", + "value": {"enableAutoForwarding": True}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.gmail.gmail_service import ( + Gmail, + ) + + gmail = Gmail(mock_provider) + + assert gmail.policies_fetched is True + assert gmail.policies.enable_auto_forwarding is False + + def test_off_for_everyone_when_off_at_the_root_with_no_override(self): + """A root-level DISABLED status with no org unit or group override is off for everyone""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": { + "orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}", + "sortOrder": 201.00122, + }, + "setting": { + "type": "settings/gmail.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.gmail.gmail_service import ( + Gmail, + ) + + fetched = Gmail(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is False + assert fetched.policies.off_for_everyone is True + + def test_reads_the_service_status_and_its_overrides(self): + """Off at the customer level only counts when no org unit or group turns the service back on""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00122}, + "setting": { + "type": "settings/gmail.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + { + "type": "ADMIN", + "policyQuery": {"orgUnit": "orgUnits/sub", "sortOrder": 301.00122}, + "setting": { + "type": "settings/gmail.service_status", + "value": {"serviceState": "ENABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.gmail.gmail_service import ( + Gmail, + ) + + fetched = Gmail(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is True + assert fetched.policies.off_for_everyone is False + def test_gmail_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py index 11fbe29098..80232cd87a 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -91,6 +92,38 @@ class TestGmailShortenerScanningEnabled: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_shortener_scanning_enabled.gmail_shortener_scanning_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_shortener_scanning_enabled.gmail_shortener_scanning_enabled import ( + gmail_shortener_scanning_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", enable_shortener_scanning=False + ) + + findings = gmail_shortener_scanning_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled_test.py index 3a8fd0d337..56484f8f4d 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -127,6 +128,38 @@ class TestGmailUnauthenticatedEmailProtectionEnabled: assert findings[0].status == "FAIL" assert "insecure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled import ( + gmail_unauthenticated_email_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", detect_unauthenticated_emails=False + ) + + findings = gmail_unauthenticated_email_protection_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py index 70fc539836..3d3cdde1d7 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py @@ -3,6 +3,7 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -97,6 +98,39 @@ class TestGmailUntrustedLinkWarningsEnabled: assert findings[0].status == "FAIL" assert "insecure default" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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_untrusted_link_warnings_enabled.gmail_untrusted_link_warnings_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_untrusted_link_warnings_enabled.gmail_untrusted_link_warnings_enabled import ( + gmail_untrusted_link_warnings_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + service_state="DISABLED", + enable_aggressive_warnings_on_untrusted_links=False, + ) + + findings = gmail_untrusted_link_warnings_enabled().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Gmail is turned off for everyone in domain {DOMAIN}, " + f"so no user mailbox receives mail through Gmail." + ) + def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted_test.py b/tests/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted_test.py index b91457f7b0..1344653a50 100644 --- a/tests/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted_test.py +++ b/tests/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted_test.py @@ -5,6 +5,7 @@ from prowler.providers.googleworkspace.services.groups.groups_service import ( ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -244,6 +245,80 @@ class TestGroupsCreationRestricted: assert "external members" in findings[0].status_extended assert "incoming email" in findings[0].status_extended + def test_pass_when_the_service_is_off_and_existing_groups_are_restricted(self): + """Off for everyone makes creation moot; existing groups still have to keep outsiders out""" + 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.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + service_state="DISABLED", + create_groups_access_level="USERS_IN_DOMAIN", + owners_can_allow_external_members=False, + owners_can_allow_incoming_mail_from_public=False, + ) + + findings = groups_creation_restricted().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Groups for Business is turned off for everyone in domain {DOMAIN}, " + f"so no group can be created, and existing groups allow no external " + f"members and no incoming email from outside." + ) + + def test_fail_when_the_service_is_off_but_existing_groups_accept_outsiders(self): + """Existing groups stay active when the service is off, so their email boundary still counts""" + 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.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + service_state="DISABLED", + create_groups_access_level="USERS_IN_DOMAIN", + owners_can_allow_external_members=True, + owners_can_allow_incoming_mail_from_public=True, + ) + + findings = groups_creation_restricted().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "group creation" not in findings[0].status_extended + assert findings[0].status_extended == ( + f"Groups for Business is turned off for everyone in domain {DOMAIN}, " + f"so no group can be created, but existing groups remain active and " + f"are not fully restricted: group owners can allow external members; " + f"group owners can allow incoming email from outside the organization." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted_test.py b/tests/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted_test.py index ec25683728..fce5ebcc33 100644 --- a/tests/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted_test.py +++ b/tests/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted_test.py @@ -105,6 +105,35 @@ class TestGroupsExternalAccessRestricted: assert findings[0].status == "PASS" assert "secure default" in findings[0].status_extended + def test_evaluates_the_setting_when_the_service_is_off_for_everyone(self): + """Existing groups stay active when Groups for Business is off, so the stored setting still counts""" + 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.groups.groups_external_access_restricted.groups_external_access_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted import ( + groups_external_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + service_state="DISABLED", collaboration_capability="ANYONE_CAN_POST" + ) + + findings = groups_external_access_restricted().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "turned off" not in findings[0].status_extended + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/groups/groups_service_test.py b/tests/providers/googleworkspace/services/groups/groups_service_test.py index 86b031a8a2..a00af52482 100644 --- a/tests/providers/googleworkspace/services/groups/groups_service_test.py +++ b/tests/providers/googleworkspace/services/groups/groups_service_test.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch from tests.providers.googleworkspace.googleworkspace_fixtures import ( + ROOT_ORG_UNIT_ID, set_mocked_googleworkspace_provider, ) @@ -64,6 +65,164 @@ class TestGroupsService: assert groups.policies.owners_can_hide_groups is False assert groups.policies.new_groups_are_hidden is False + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/groups_for_business.groups_sharing", + "value": {"createGroupsAccessLevel": "ADMIN_ONLY"}, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.00002}, + "setting": { + "type": "settings/groups_for_business.groups_sharing", + "value": {"createGroupsAccessLevel": "USERS_IN_DOMAIN"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + groups = Groups(mock_provider) + + assert groups.policies_fetched is True + assert groups.policies.create_groups_access_level == "ADMIN_ONLY" + + def test_off_for_everyone_when_off_at_the_root_with_no_override(self): + """A root-level DISABLED status with no org unit or group override is off for everyone""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": { + "orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}", + "sortOrder": 201.00122, + }, + "setting": { + "type": "settings/groups_for_business.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + fetched = Groups(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is False + assert fetched.policies.off_for_everyone is True + + def test_reads_the_service_status_and_its_overrides(self): + """Off at the customer level only counts when no org unit or group turns the service back on""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00122}, + "setting": { + "type": "settings/groups_for_business.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + { + "type": "ADMIN", + "policyQuery": {"orgUnit": "orgUnits/sub", "sortOrder": 301.00122}, + "setting": { + "type": "settings/groups_for_business.service_status", + "value": {"serviceState": "ENABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + fetched = Groups(mock_provider) + + assert fetched.policies_fetched is True + assert fetched.policies.service_state == "DISABLED" + assert fetched.policies.service_state_overridden is True + assert fetched.policies.off_for_everyone is False + def test_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted_test.py b/tests/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted_test.py index 761073cc89..f4105c4e3b 100644 --- a/tests/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted_test.py +++ b/tests/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted_test.py @@ -5,6 +5,7 @@ from prowler.providers.googleworkspace.services.groups.groups_service import ( ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, + DOMAIN, set_mocked_googleworkspace_provider, ) @@ -138,6 +139,39 @@ class TestGroupsViewConversationsRestricted: assert "default" in findings[0].status_extended assert "all organization users" in findings[0].status_extended + def test_pass_when_the_service_is_off_for_everyone(self): + """A setting that only acts while the service is on has no effect once the service is off""" + 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.groups.groups_view_conversations_restricted.groups_view_conversations_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted import ( + groups_view_conversations_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + service_state="DISABLED", + view_topics_default_access_level="DOMAIN_USERS", + ) + + findings = groups_view_conversations_restricted().execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].status_extended == ( + f"Groups for Business is turned off for everyone in domain {DOMAIN}, " + f"so no group conversation can be viewed in the Google Groups app." + ) + def test_no_findings_when_fetch_failed(self): """Test no findings returned when the API fetch failed""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py b/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py index 8c549cd0d7..cac2143cd7 100644 --- a/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py +++ b/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py @@ -142,6 +142,59 @@ class TestMarketplaceService: assert marketplace.policies_fetched is True assert marketplace.policies.access_level == "ALLOW_NONE" + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/workspace_marketplace.apps_access_options", + "value": {"accessLevel": "ALLOW_LISTED_APPS"}, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.00002}, + "setting": { + "type": "settings/workspace_marketplace.apps_access_options", + "value": {"accessLevel": "ALLOW_ALL"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is True + assert marketplace.policies.access_level == "ALLOW_LISTED_APPS" + def test_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py b/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py index e17614de0d..8469271321 100644 --- a/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py +++ b/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py @@ -239,6 +239,115 @@ class TestSecurityService: assert security.policies.google_services_restricted is True assert security.policies.dlp_drive_rules_exist is True + def test_the_admin_policy_outranks_the_system_defaults_returned_after_it(self): + """The API returns Google's SYSTEM defaults next to the ADMIN policy saved in the console. + + The greatest sortOrder is the value in force, whatever the order the + API lists them in. This is the shape of a real tenant, where the last + policy returned used to overwrite the admin's password policy. + """ + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + root_ou = f"orgUnits/{ROOT_ORG_UNIT_ID}" + mock_security_list = MagicMock() + mock_security_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"orgUnit": root_ou, "sortOrder": 201.00131}, + "setting": { + "type": "settings/security.password", + "value": { + "allowedStrength": "STRONG", + "minimumLength": 12, + "maximumLength": 100, + "enforceRequirementsAtLogin": True, + "allowReuse": False, + "expirationDuration": "15552000s", + }, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"orgUnit": root_ou, "sortOrder": 100.00001}, + "setting": { + "type": "settings/security.password", + "value": { + "allowedStrength": "STRONG", + "minimumLength": 8, + "maximumLength": 100, + "enforceRequirementsAtLogin": False, + "allowReuse": False, + "expirationDuration": "0s", + }, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"orgUnit": root_ou, "sortOrder": 100.00003}, + "setting": { + "type": "settings/security.password", + "value": { + "allowedStrength": "STRONG", + "minimumLength": 8, + "maximumLength": 100, + "enforceRequirementsAtLogin": False, + "allowReuse": False, + "expirationDuration": "0s", + }, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"orgUnit": root_ou, "sortOrder": 100.00003}, + "setting": { + "type": "settings/security.session_controls", + "value": {"webSessionDuration": "1209600s"}, + }, + }, + ] + } + mock_api_controls_list = MagicMock() + mock_api_controls_list.execute.return_value = {"policies": []} + mock_dlp_list = MagicMock() + mock_dlp_list.execute.return_value = {"policies": []} + mock_service = MagicMock() + mock_service.policies().list.side_effect = [ + mock_security_list, + mock_api_controls_list, + mock_dlp_list, + ] + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is True + assert security.policies.password_minimum_length == 12 + assert security.policies.password_enforce_at_login is True + assert security.policies.password_allow_reuse is False + assert security.policies.password_expiration_duration == "15552000s" + assert security.policies.web_session_duration == "1209600s" + assert security.policies.overridden_settings == [] + def test_fetch_policies_empty_response(self): """Test handling empty policies response across all namespaces""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/sites/sites_service_test.py b/tests/providers/googleworkspace/services/sites/sites_service_test.py index 63a473238d..f381195c3c 100644 --- a/tests/providers/googleworkspace/services/sites/sites_service_test.py +++ b/tests/providers/googleworkspace/services/sites/sites_service_test.py @@ -97,6 +97,59 @@ class TestSitesService: assert sites.policies_fetched is True assert sites.policies.service_state == "ENABLED" + def test_the_admin_policy_outranks_a_system_default_returned_after_it(self): + """The greatest sortOrder is the value in force, whatever order the API lists them in""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "type": "ADMIN", + "policyQuery": {"sortOrder": 201.00131}, + "setting": { + "type": "settings/sites.service_status", + "value": {"serviceState": "DISABLED"}, + }, + }, + { + "type": "SYSTEM", + "policyQuery": {"sortOrder": 100.00005}, + "setting": { + "type": "settings/sites.service_status", + "value": {"serviceState": "ENABLED"}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is True + assert sites.policies.service_state == "DISABLED" + def test_fetch_policies_empty_response(self): """Test handling empty policies response""" mock_provider = set_mocked_googleworkspace_provider()