From 74622dd576ea69f5fe96516b950507fb468bf463 Mon Sep 17 00:00:00 2001 From: lydiavilchez <114735608+lydiavilchez@users.noreply.github.com> Date: Thu, 21 May 2026 15:52:15 +0200 Subject: [PATCH] feat(googleworkspace): add sites, additional_services and marketplace service checks (#11281) --- prowler/CHANGELOG.md | 1 + .../cis_1.3_googleworkspace.json | 12 +- .../cisa_scuba_0.6_googleworkspace.json | 8 +- .../services/additionalservices/__init__.py | 0 .../additionalservices_client.py | 6 + .../__init__.py | 0 ...ces_external_groups_disabled.metadata.json | 37 +++ ...tionalservices_external_groups_disabled.py | 55 ++++ .../additionalservices_service.py | 92 ++++++ .../services/marketplace/__init__.py | 0 .../__init__.py | 0 ...place_apps_access_restricted.metadata.json | 37 +++ .../marketplace_apps_access_restricted.py | 61 ++++ .../marketplace/marketplace_client.py | 6 + .../marketplace/marketplace_service.py | 89 ++++++ .../services/sites/__init__.py | 0 .../services/sites/sites_client.py | 6 + .../services/sites/sites_service.py | 88 ++++++ .../sites/sites_service_disabled/__init__.py | 0 .../sites_service_disabled.metadata.json | 37 +++ .../sites_service_disabled.py | 52 ++++ ...lservices_external_groups_disabled_test.py | 128 ++++++++ .../additionalservices_service_test.py | 234 +++++++++++++++ ...marketplace_apps_access_restricted_test.py | 152 ++++++++++ .../marketplace/marketplace_service_test.py | 279 ++++++++++++++++++ .../sites_service_disabled_test.py | 124 ++++++++ .../services/sites/sites_service_test.py | 234 +++++++++++++++ 27 files changed, 1733 insertions(+), 5 deletions(-) create mode 100644 prowler/providers/googleworkspace/services/additionalservices/__init__.py create mode 100644 prowler/providers/googleworkspace/services/additionalservices/additionalservices_client.py create mode 100644 prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/__init__.py create mode 100644 prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.metadata.json create mode 100644 prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.py create mode 100644 prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py create mode 100644 prowler/providers/googleworkspace/services/marketplace/__init__.py create mode 100644 prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/__init__.py create mode 100644 prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.metadata.json create mode 100644 prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.py create mode 100644 prowler/providers/googleworkspace/services/marketplace/marketplace_client.py create mode 100644 prowler/providers/googleworkspace/services/marketplace/marketplace_service.py create mode 100644 prowler/providers/googleworkspace/services/sites/__init__.py create mode 100644 prowler/providers/googleworkspace/services/sites/sites_client.py create mode 100644 prowler/providers/googleworkspace/services/sites/sites_service.py create mode 100644 prowler/providers/googleworkspace/services/sites/sites_service_disabled/__init__.py create mode 100644 prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.metadata.json create mode 100644 prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.py create mode 100644 tests/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled_test.py create mode 100644 tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py create mode 100644 tests/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted_test.py create mode 100644 tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py create mode 100644 tests/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled_test.py create mode 100644 tests/providers/googleworkspace/services/sites/sites_service_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index b3916f16b8..95b4ee6712 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🚀 Added +- Sites, Additional Google services, and Marketplace checks for Google Workspace provider using the Cloud Identity Policy API [(#11281)](https://github.com/prowler-cloud/prowler/pull/11281) - `entra_app_registration_client_secret_unused` check for M365 provider [(#11232)](https://github.com/prowler-cloud/prowler/pull/11232) - `cloudsql_instance_cmek_encryption_enabled` check for GCP provider [(#11023)](https://github.com/prowler-cloud/prowler/pull/11023) - Google Workspace Groups service with 3 new checks [(#11186)](https://github.com/prowler-cloud/prowler/pull/11186) diff --git a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json index 95e07874af..0867da103e 100644 --- a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json @@ -1291,7 +1291,9 @@ { "Id": "3.1.7.1", "Description": "Ensure service status for Google Sites is set to off", - "Checks": [], + "Checks": [ + "sites_service_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1312,7 +1314,9 @@ { "Id": "3.1.8.1", "Description": "Ensure access to external Google Groups is OFF for Everyone", - "Checks": [], + "Checks": [ + "additionalservices_external_groups_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1333,7 +1337,9 @@ { "Id": "3.1.9.1.1", "Description": "Ensure users access to Google Workspace Marketplace apps is restricted", - "Checks": [], + "Checks": [ + "marketplace_apps_access_restricted" + ], "Attributes": [ { "Section": "3 Apps", diff --git a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json index b0ba52cefe..0ce4d2ef6a 100644 --- a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json @@ -374,7 +374,9 @@ { "Id": "GWS.COMMONCONTROLS.11.1", "Description": "Only approved Marketplace apps SHALL be allowed for installation", - "Checks": [], + "Checks": [ + "marketplace_apps_access_restricted" + ], "Attributes": [ { "Section": "Common Controls", @@ -1714,7 +1716,9 @@ { "Id": "GWS.SITES.1.1", "Description": "Sites Service SHOULD be disabled for all users", - "Checks": [], + "Checks": [ + "sites_service_disabled" + ], "Attributes": [ { "Section": "Sites", diff --git a/prowler/providers/googleworkspace/services/additionalservices/__init__.py b/prowler/providers/googleworkspace/services/additionalservices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_client.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_client.py new file mode 100644 index 0000000000..0d256528fa --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, +) + +additionalservices_client = AdditionalServices(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/__init__.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.metadata.json b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.metadata.json new file mode 100644 index 0000000000..82bb368a8a --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "additionalservices_external_groups_disabled", + "CheckTitle": "Access to external Google Groups is off for everyone", + "CheckType": [], + "ServiceName": "additionalservices", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The Additional Google services configuration **disables access to external Google Groups** for all users. This setting controls whether users can access groups created outside the organization from their Google Workspace account.", + "Risk": "When external Google Groups access is enabled, users can access and participate in groups created **outside the organization**, potentially exposing them to **phishing, social engineering, or data leakage** through unmanaged external group communications.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/181865", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Additional Google services**\n3. Scroll down to **Google Groups**\n4. Set it to **OFF for everyone**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable access to **external Google Groups** for all users. If specific users require access to external groups, enable it by exception for those users or groups only.", + "Url": "https://hub.prowler.com/check/additionalservices_external_groups_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check covers the 'Additional Google services > Google Groups' toggle, which is distinct from the 'Groups for Business' sharing settings covered by the groups service checks." +} diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.py new file mode 100644 index 0000000000..cf61220054 --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.py @@ -0,0 +1,55 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.additionalservices.additionalservices_client import ( + additionalservices_client, +) + + +class additionalservices_external_groups_disabled(Check): + """Check that access to external Google Groups is disabled for all users. + + This check verifies that the domain-level Additional Google services policy + disables external Google Groups access, preventing users from accessing + groups created outside the organization. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if additionalservices_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=additionalservices_client.policies, + resource_id="additionalServicesPolicies", + resource_name="Additional Services Policies", + customer_id=additionalservices_client.provider.identity.customer_id, + ) + + groups_state = additionalservices_client.policies.groups_service_state + + if groups_state == "DISABLED": + report.status = "PASS" + report.status_extended = ( + f"Access to external Google Groups is disabled " + f"in domain {additionalservices_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if groups_state is None: + report.status_extended = ( + f"Access to external Google Groups is not explicitly configured " + f"in domain {additionalservices_client.provider.identity.domain}. " + f"The default is ON for everyone. " + f"External Google Groups access should be disabled." + ) + else: + report.status_extended = ( + f"Access to external Google Groups is enabled " + f"in domain {additionalservices_client.provider.identity.domain}. " + f"External Google Groups access should be disabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py new file mode 100644 index 0000000000..5a41060bc8 --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py @@ -0,0 +1,92 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class AdditionalServices(GoogleWorkspaceService): + """Google Workspace Additional Services for auditing domain-level service toggles. + + Uses the Cloud Identity Policy API v1 to read the service status of + additional Google services configured in the Admin Console, such as + the external Google Groups access toggle. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = AdditionalServicesPolicies() + self.policies_fetched = False + self._fetch_additional_services_policies() + + def _fetch_additional_services_policies(self): + """Fetch Additional Services policies from the Cloud Identity Policy API v1.""" + logger.info("Additional Services - Fetching policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("groups.service_status")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + 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, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Additional Services policies fetched - " + f"Groups service state: {self.policies.groups_service_state}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Additional Services policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class AdditionalServicesPolicies(BaseModel): + """Model for domain-level Additional Google Services policy settings.""" + + # groups.service_status + groups_service_state: Optional[str] = None diff --git a/prowler/providers/googleworkspace/services/marketplace/__init__.py b/prowler/providers/googleworkspace/services/marketplace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/__init__.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.metadata.json b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.metadata.json new file mode 100644 index 0000000000..7d982370a1 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "marketplace_apps_access_restricted", + "CheckTitle": "Users access to Google Workspace Marketplace apps is restricted", + "CheckType": [], + "ServiceName": "marketplace", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide Google Workspace Marketplace configuration **restricts which apps users can install**. Only admin-approved apps from the Marketplace allowlist are permitted, preventing users from installing unvetted third-party applications.", + "Risk": "Allowing unrestricted Marketplace app installation exposes the organization to **unvetted third-party applications** that may request broad OAuth scopes, potentially gaining access to **sensitive organizational data** including emails, documents, and calendar events without proper security review.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/6089179", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace Marketplace apps**\n3. Click **Settings**\n4. Under **Manage Google Workspace Marketplace allowlist access**, select **Allow users to install and run only selected apps from the Marketplace**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict **Marketplace app installation** to only **admin-approved apps** to prevent users from installing unvetted third-party applications that could access sensitive organizational data.", + "Url": "https://hub.prowler.com/check/marketplace_apps_access_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.py new file mode 100644 index 0000000000..cee1adaa71 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.py @@ -0,0 +1,61 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.marketplace.marketplace_client import ( + marketplace_client, +) + + +class marketplace_apps_access_restricted(Check): + """Check that Google Workspace Marketplace app installation is restricted. + + This check verifies that the domain-level Marketplace policy restricts + which apps users can install, preventing unvetted third-party applications + from accessing organizational data. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if marketplace_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=marketplace_client.policies, + resource_id="marketplacePolicies", + resource_name="Marketplace Policies", + customer_id=marketplace_client.provider.identity.customer_id, + ) + + access_level = marketplace_client.policies.access_level + + if access_level == "ALLOW_LISTED_APPS": + report.status = "PASS" + report.status_extended = ( + f"Marketplace app installation is restricted to admin-approved apps " + f"in domain {marketplace_client.provider.identity.domain}." + ) + elif access_level == "ALLOW_NONE": + report.status = "PASS" + report.status_extended = ( + f"Marketplace app installation is fully blocked " + f"in domain {marketplace_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if access_level is None: + report.status_extended = ( + f"Marketplace app access is not explicitly configured " + f"in domain {marketplace_client.provider.identity.domain}. " + f"The default allows all apps. " + f"App installation should be restricted to approved apps only." + ) + else: + report.status_extended = ( + f"Marketplace allows users to install any app " + f"in domain {marketplace_client.provider.identity.domain}. " + f"App installation should be restricted to approved apps only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_client.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_client.py new file mode 100644 index 0000000000..ccc6cdf208 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, +) + +marketplace_client = Marketplace(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py new file mode 100644 index 0000000000..4c117a4468 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py @@ -0,0 +1,89 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Marketplace(GoogleWorkspaceService): + """Google Workspace Marketplace service for auditing domain-level Marketplace policies. + + Uses the Cloud Identity Policy API v1 to read the Marketplace app access + settings configured in the Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = MarketplacePolicies() + self.policies_fetched = False + self._fetch_marketplace_policies() + + def _fetch_marketplace_policies(self): + """Fetch Marketplace policies from the Cloud Identity Policy API v1.""" + logger.info("Marketplace - Fetching marketplace policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("workspace_marketplace.*")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + 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, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Marketplace policies fetched - " + f"Access level: {self.policies.access_level}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Marketplace policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class MarketplacePolicies(BaseModel): + """Model for domain-level Marketplace policy settings.""" + + # workspace_marketplace.apps_access_options + access_level: Optional[str] = None diff --git a/prowler/providers/googleworkspace/services/sites/__init__.py b/prowler/providers/googleworkspace/services/sites/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/sites/sites_client.py b/prowler/providers/googleworkspace/services/sites/sites_client.py new file mode 100644 index 0000000000..d57e537c5d --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, +) + +sites_client = Sites(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/sites/sites_service.py b/prowler/providers/googleworkspace/services/sites/sites_service.py new file mode 100644 index 0000000000..456c2aca65 --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_service.py @@ -0,0 +1,88 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Sites(GoogleWorkspaceService): + """Google Workspace Sites service for auditing domain-level Sites policies. + + Uses the Cloud Identity Policy API v1 to read the Sites service status + configured in the Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = SitesPolicies() + self.policies_fetched = False + self._fetch_sites_policies() + + def _fetch_sites_policies(self): + """Fetch Sites policies from the Cloud Identity Policy API v1.""" + logger.info("Sites - Fetching sites policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("sites.*")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + 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, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Sites policies fetched - " + f"Service state: {self.policies.service_state}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Sites policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class SitesPolicies(BaseModel): + """Model for domain-level Sites policy settings.""" + + # sites.service_status + service_state: Optional[str] = None diff --git a/prowler/providers/googleworkspace/services/sites/sites_service_disabled/__init__.py b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.metadata.json b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.metadata.json new file mode 100644 index 0000000000..6422fa974b --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "sites_service_disabled", + "CheckTitle": "Service status for Google Sites is set to off", + "CheckType": [], + "ServiceName": "sites", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide Google Sites configuration has the service **disabled for all users**. Google Sites allows users to create internal and external websites, which may expose sensitive information or create unmanaged content outside the organization's control.", + "Risk": "When Google Sites is enabled, users can create websites that may **inadvertently expose internal information** to external parties. These sites can be difficult to track and manage, creating potential **data leakage vectors** outside the organization's standard content management controls.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/182442", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Sites**\n3. Click **Service status**\n4. Select **OFF for everyone**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **Google Sites** for all users to reduce the organization's attack surface. If specific users require access, enable it by exception for those users or groups only.", + "Url": "https://hub.prowler.com/check/sites_service_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.py b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.py new file mode 100644 index 0000000000..2acd84fb7c --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.sites.sites_client import sites_client + + +class sites_service_disabled(Check): + """Check that the Google Sites service is disabled for all users. + + This check verifies that the domain-level policy disables the Google Sites + service, reducing the organization's attack surface by preventing users + from creating internal or external websites. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if sites_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=sites_client.policies, + resource_id="sitesPolicies", + resource_name="Sites Policies", + customer_id=sites_client.provider.identity.customer_id, + ) + + service_state = sites_client.policies.service_state + + if service_state == "DISABLED": + report.status = "PASS" + report.status_extended = ( + f"Google Sites service is disabled " + f"in domain {sites_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if service_state is None: + report.status_extended = ( + f"Google Sites service is not explicitly configured " + f"in domain {sites_client.provider.identity.domain}. " + f"The default is ON for everyone. Google Sites should be disabled." + ) + else: + report.status_extended = ( + f"Google Sites service is enabled " + f"in domain {sites_client.provider.identity.domain}. " + f"Google Sites should be disabled." + ) + + findings.append(report) + + return findings diff --git a/tests/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled_test.py b/tests/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled_test.py new file mode 100644 index 0000000000..662f5324cd --- /dev/null +++ b/tests/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled_test.py @@ -0,0 +1,128 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServicesPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestAdditionalServicesExternalGroupsDisabled: + def test_pass_groups_disabled(self): + """Test PASS when external Google Groups access is disabled""" + 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.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = AdditionalServicesPolicies( + groups_service_state="DISABLED" + ) + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Additional Services Policies" + assert findings[0].resource_id == "additionalServicesPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_groups_enabled(self): + """Test FAIL when external Google Groups access is enabled""" + 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.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = AdditionalServicesPolicies( + groups_service_state="ENABLED" + ) + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) - Google default is ON (insecure)""" + 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.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = AdditionalServicesPolicies(groups_service_state=None) + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" 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() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = AdditionalServicesPolicies() + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py b/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py new file mode 100644 index 0000000000..85d9afea45 --- /dev/null +++ b/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py @@ -0,0 +1,234 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestAdditionalServicesService: + def test_fetch_policies_groups_off(self): + """Test fetching Additional Services policy with Groups OFF""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/groups.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.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_groups_on(self): + """Test fetching Additional Services policy with Groups 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": [ + { + "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 == "ENABLED" + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response""" + 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": []} + 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 is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + 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_service.policies().list.side_effect = Exception("API Error") + + 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 False + assert additional_services.policies.groups_service_state is None + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + 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 + + 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=None, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is False + assert additional_services.policies.groups_service_state is None + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + 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_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + 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 False + assert additional_services.policies.groups_service_state is None + + def test_additional_services_policies_model(self): + """Test AdditionalServicesPolicies Pydantic model""" + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServicesPolicies, + ) + + policies = AdditionalServicesPolicies(groups_service_state="DISABLED") + + assert policies.groups_service_state == "DISABLED" diff --git a/tests/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted_test.py b/tests/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted_test.py new file mode 100644 index 0000000000..8da2a4326d --- /dev/null +++ b/tests/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted_test.py @@ -0,0 +1,152 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + MarketplacePolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestMarketplaceAppsAccessRestricted: + def test_pass_allow_listed_apps(self): + """Test PASS when Marketplace access is restricted to admin-approved apps""" + 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.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level="ALLOW_LISTED_APPS") + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "restricted" in findings[0].status_extended + assert findings[0].resource_name == "Marketplace Policies" + assert findings[0].resource_id == "marketplacePolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_allow_none(self): + """Test PASS when Marketplace app installation is fully blocked""" + 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.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level="ALLOW_NONE") + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "blocked" in findings[0].status_extended + + def test_fail_allow_all(self): + """Test FAIL when Marketplace allows all apps""" + 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.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level="ALLOW_ALL") + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "any app" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) - Google default is ALLOW_ALL (insecure)""" + 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.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level=None) + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" 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() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = MarketplacePolicies() + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py b/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py new file mode 100644 index 0000000000..8c549cd0d7 --- /dev/null +++ b/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py @@ -0,0 +1,279 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestMarketplaceService: + def test_fetch_policies_allow_all(self): + """Test fetching Marketplace policy with ALLOW_ALL access level""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "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_ALL" + + def test_fetch_policies_allow_listed_apps(self): + """Test fetching Marketplace policy with ALLOW_LISTED_APPS access level""" + 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": [ + { + "setting": { + "type": "settings/workspace_marketplace.apps_access_options", + "value": { + "accessLevel": "ALLOW_LISTED_APPS", + }, + } + }, + ] + } + 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_allow_none(self): + """Test fetching Marketplace policy with ALLOW_NONE access level""" + 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": [ + { + "setting": { + "type": "settings/workspace_marketplace.apps_access_options", + "value": { + "accessLevel": "ALLOW_NONE", + }, + } + }, + ] + } + 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_NONE" + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response""" + 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": []} + 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 is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + 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_service.policies().list.side_effect = Exception("API Error") + + 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 False + assert marketplace.policies.access_level is None + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + 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 + + 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=None, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is False + assert marketplace.policies.access_level is None + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + 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_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + 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 False + assert marketplace.policies.access_level is None + + def test_marketplace_policies_model(self): + """Test MarketplacePolicies Pydantic model""" + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + MarketplacePolicies, + ) + + policies = MarketplacePolicies(access_level="ALLOW_LISTED_APPS") + + assert policies.access_level == "ALLOW_LISTED_APPS" diff --git a/tests/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled_test.py b/tests/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled_test.py new file mode 100644 index 0000000000..ff668f5cf5 --- /dev/null +++ b/tests/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled_test.py @@ -0,0 +1,124 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.sites.sites_service import ( + SitesPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSitesServiceDisabled: + def test_pass_service_disabled(self): + """Test PASS when Sites service is disabled (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.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SitesPolicies(service_state="DISABLED") + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Sites Policies" + assert findings[0].resource_id == "sitesPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_service_enabled(self): + """Test FAIL when Sites service is enabled (ON 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.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SitesPolicies(service_state="ENABLED") + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) - Google default is ON (insecure)""" + 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.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SitesPolicies(service_state=None) + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" 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() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SitesPolicies() + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/sites/sites_service_test.py b/tests/providers/googleworkspace/services/sites/sites_service_test.py new file mode 100644 index 0000000000..63a473238d --- /dev/null +++ b/tests/providers/googleworkspace/services/sites/sites_service_test.py @@ -0,0 +1,234 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestSitesService: + def test_fetch_policies_service_status_off(self): + """Test fetching Sites policy with service status OFF""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/sites.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.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_service_status_on(self): + """Test fetching Sites policy with service status 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": [ + { + "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 == "ENABLED" + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response""" + 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": []} + 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 is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + 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_service.policies().list.side_effect = Exception("API Error") + + 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 False + assert sites.policies.service_state is None + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + 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 + + 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=None, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is False + assert sites.policies.service_state is None + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + 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_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + 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 False + assert sites.policies.service_state is None + + def test_sites_policies_model(self): + """Test SitesPolicies Pydantic model""" + from prowler.providers.googleworkspace.services.sites.sites_service import ( + SitesPolicies, + ) + + policies = SitesPolicies(service_state="DISABLED") + + assert policies.service_state == "DISABLED"