From bc38104903f6944d15216b65e4813de40951441e Mon Sep 17 00:00:00 2001
From: lydiavilchez <114735608+lydiavilchez@users.noreply.github.com>
Date: Wed, 8 Apr 2026 13:26:56 +0200
Subject: [PATCH] feat(googleworkspace): add calendar service checks using
Cloud Identity Policy API (#10597)
---
.../googleworkspace/authentication.mdx | 39 ++-
prowler/CHANGELOG.md | 1 +
.../cis_1.3_googleworkspace.json | 12 +-
.../cisa_scuba_0.6_googleworkspace.json | 12 +-
.../googleworkspace_provider.py | 16 +-
.../services/calendar/__init__.py | 0
.../services/calendar/calendar_client.py | 6 +
.../__init__.py | 0
...external_invitations_warning.metadata.json | 41 ++++
.../calendar_external_invitations_warning.py | 56 +++++
.../__init__.py | 0
...nal_sharing_primary_calendar.metadata.json | 41 ++++
...endar_external_sharing_primary_calendar.py | 56 +++++
.../__init__.py | 0
...l_sharing_secondary_calendar.metadata.json | 41 ++++
...dar_external_sharing_secondary_calendar.py | 56 +++++
.../services/calendar/calendar_service.py | 112 +++++++++
...endar_external_invitations_warning_test.py | 130 ++++++++++
..._external_sharing_primary_calendar_test.py | 161 ++++++++++++
...xternal_sharing_secondary_calendar_test.py | 161 ++++++++++++
.../calendar/calendar_service_test.py | 231 ++++++++++++++++++
21 files changed, 1150 insertions(+), 22 deletions(-)
create mode 100644 prowler/providers/googleworkspace/services/calendar/__init__.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_client.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/__init__.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/__init__.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/__init__.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py
create mode 100644 prowler/providers/googleworkspace/services/calendar/calendar_service.py
create mode 100644 tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py
create mode 100644 tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py
create mode 100644 tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py
create mode 100644 tests/providers/googleworkspace/services/calendar/calendar_service_test.py
diff --git a/docs/user-guide/providers/googleworkspace/authentication.mdx b/docs/user-guide/providers/googleworkspace/authentication.mdx
index e5c3527f89..b070c443b3 100644
--- a/docs/user-guide/providers/googleworkspace/authentication.mdx
+++ b/docs/user-guide/providers/googleworkspace/authentication.mdx
@@ -6,17 +6,18 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
-Prowler for Google Workspace uses a **Service Account with Domain-Wide Delegation** to authenticate to the Google Workspace Admin SDK. This allows Prowler to read directory data on behalf of a super administrator without requiring an interactive login.
+Prowler for Google Workspace uses a **Service Account with Domain-Wide Delegation** to authenticate to the Google Workspace Admin SDK and the Cloud Identity Policy API. This allows Prowler to read directory data and domain-level application policies on behalf of a super administrator without requiring an interactive login.
## Required Open Authorization (OAuth) Scopes
-Prowler requests the following read-only OAuth 2.0 scopes from the Google Workspace Admin SDK:
+Prowler requests the following read-only OAuth 2.0 scopes:
| Scope | Description |
|-------|-------------|
| `https://www.googleapis.com/auth/admin.directory.user.readonly` | Read access to user accounts and their admin status |
| `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information |
| `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) |
+| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar service checks) |
| `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments |
@@ -31,13 +32,24 @@ If no GCP project exists, create one at [https://console.cloud.google.com](https
The project is only used to host the Service Account — it does not need to have any Google Workspace data in it.
-### Step 2: Enable the Admin SDK API
+### Step 2: Enable Required APIs
-1. Navigate to the [Google Cloud Console](https://console.cloud.google.com)
-2. Select the target project
-3. Navigate to **APIs & Services → Library**
-4. Search for **Admin SDK API**
-5. Click **Enable**
+In the [Google Cloud Console](https://console.cloud.google.com), select the target project and navigate to **APIs & Services → Library**. Search for and enable each of the following APIs:
+
+| API | Required For |
+|-----|--------------|
+| **Admin SDK API** | Directory service checks (users, roles, domains) |
+| **Cloud Identity API** | Calendar service checks (domain-level sharing and invitation policies) |
+
+For each API:
+
+1. Search for the API name in the library
+2. Click the API result
+3. Click **Enable**
+
+
+Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar checks will return no findings if the Cloud Identity API is not enabled.
+
### Step 3: Create a Service Account
@@ -74,7 +86,7 @@ This JSON key grants access to your Google Workspace organization. Never commit
6. In the **OAuth scopes** field, enter the following scopes as a comma-separated list:
```
-https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly,https://www.googleapis.com/auth/admin.directory.customer.readonly,https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
+https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly,https://www.googleapis.com/auth/admin.directory.customer.readonly,https://www.googleapis.com/auth/cloud-identity.policies.readonly,https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
```
7. Click **Authorize**
@@ -162,3 +174,12 @@ If Prowler connects but returns empty results or permission errors for specific
- Confirm Domain-Wide Delegation is fully propagated (wait a few minutes after setup)
- Verify all scopes are authorized in the Admin Console
- Ensure the delegated user is an active super administrator
+
+### Calendar Checks Return No Findings
+
+If the Directory checks run successfully but the Calendar checks (e.g., `calendar_external_sharing_primary_calendar`) return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
+
+- The **Cloud Identity API** is enabled in the GCP project hosting the Service Account (Step 2)
+- The scope `https://www.googleapis.com/auth/cloud-identity.policies.readonly` is included in the Domain-Wide Delegation OAuth scopes list in the Admin Console (Step 5)
+- The delegated user is a super administrator (the Policy API only returns data to super admins)
+- Domain-Wide Delegation has had time to propagate after adding the new scope (a few minutes)
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 06d1aba5a8..56c363b9cf 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -17,6 +17,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `--resource-group` and `--list-resource-groups` CLI flags to filter checks by resource group across all providers [(#10479)](https://github.com/prowler-cloud/prowler/pull/10479)
- CISA SCuBA Google Workspace Baselines compliance [(#10466)](https://github.com/prowler-cloud/prowler/pull/10466)
- CIS Google Workspace Foundations Benchmark v1.3.0 compliance [(#10462)](https://github.com/prowler-cloud/prowler/pull/10462)
+- `calendar_external_sharing_primary_calendar`, `calendar_external_sharing_secondary_calendar`, and `calendar_external_invitations_warning` checks for Google Workspace provider using the Cloud Identity Policy API [(#10597)](https://github.com/prowler-cloud/prowler/pull/10597)
- `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222)
- `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)
diff --git a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json
index 7792bba0da..5d99d82c73 100644
--- a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json
+++ b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json
@@ -98,7 +98,9 @@
{
"Id": "3.1.1.1.1",
"Description": "Ensure external sharing options for primary calendars are configured",
- "Checks": [],
+ "Checks": [
+ "calendar_external_sharing_primary_calendar"
+ ],
"Attributes": [
{
"Section": "3 Apps",
@@ -140,7 +142,9 @@
{
"Id": "3.1.1.1.3",
"Description": "Ensure external invitation warnings for Google Calendar are configured",
- "Checks": [],
+ "Checks": [
+ "calendar_external_invitations_warning"
+ ],
"Attributes": [
{
"Section": "3 Apps",
@@ -161,7 +165,9 @@
{
"Id": "3.1.1.2.1",
"Description": "Ensure external sharing options for secondary calendars are configured",
- "Checks": [],
+ "Checks": [
+ "calendar_external_sharing_secondary_calendar"
+ ],
"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 17c2b88b2c..e81f4c70a3 100644
--- a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json
+++ b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json
@@ -1310,7 +1310,9 @@
{
"Id": "GWS.CALENDAR.1.1",
"Description": "External Sharing Options for Primary Calendars SHALL be configured to Only free/busy information (hide event details)",
- "Checks": [],
+ "Checks": [
+ "calendar_external_sharing_primary_calendar"
+ ],
"Attributes": [
{
"Section": "Calendar",
@@ -1323,7 +1325,9 @@
{
"Id": "GWS.CALENDAR.1.2",
"Description": "External sharing options for secondary calendars SHALL be configured to Only free/busy information (hide event details)",
- "Checks": [],
+ "Checks": [
+ "calendar_external_sharing_secondary_calendar"
+ ],
"Attributes": [
{
"Section": "Calendar",
@@ -1336,7 +1340,9 @@
{
"Id": "GWS.CALENDAR.2.1",
"Description": "External invitations warnings SHALL be enabled to prompt users before sending invitations",
- "Checks": [],
+ "Checks": [
+ "calendar_external_invitations_warning"
+ ],
"Attributes": [
{
"Section": "Calendar",
diff --git a/prowler/providers/googleworkspace/googleworkspace_provider.py b/prowler/providers/googleworkspace/googleworkspace_provider.py
index 549831a2fb..563078f1e3 100644
--- a/prowler/providers/googleworkspace/googleworkspace_provider.py
+++ b/prowler/providers/googleworkspace/googleworkspace_provider.py
@@ -59,11 +59,13 @@ class GoogleworkspaceProvider(Provider):
_mutelist: GoogleWorkspaceMutelist
audit_metadata: Audit_Metadata
- # Google Workspace Admin SDK OAuth2 scopes
- DIRECTORY_SCOPES = [
+ # Google Workspace OAuth2 scopes
+ SCOPES = [
"https://www.googleapis.com/auth/admin.directory.user.readonly",
"https://www.googleapis.com/auth/admin.directory.domain.readonly",
"https://www.googleapis.com/auth/admin.directory.customer.readonly",
+ # Cloud Identity Policy API (calendar and other app policies)
+ "https://www.googleapis.com/auth/cloud-identity.policies.readonly",
"https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly",
]
@@ -215,7 +217,7 @@ class GoogleworkspaceProvider(Provider):
try:
credentials = service_account.Credentials.from_service_account_file(
credentials_file,
- scopes=GoogleworkspaceProvider.DIRECTORY_SCOPES,
+ scopes=GoogleworkspaceProvider.SCOPES,
)
except FileNotFoundError as error:
raise GoogleWorkspaceInvalidCredentialsError(
@@ -242,7 +244,7 @@ class GoogleworkspaceProvider(Provider):
try:
credentials = service_account.Credentials.from_service_account_info(
credentials_data,
- scopes=GoogleworkspaceProvider.DIRECTORY_SCOPES,
+ scopes=GoogleworkspaceProvider.SCOPES,
)
except ValueError as error:
raise GoogleWorkspaceInvalidCredentialsError(
@@ -265,7 +267,7 @@ class GoogleworkspaceProvider(Provider):
try:
credentials = service_account.Credentials.from_service_account_file(
env_file,
- scopes=GoogleworkspaceProvider.DIRECTORY_SCOPES,
+ scopes=GoogleworkspaceProvider.SCOPES,
)
except FileNotFoundError as error:
raise GoogleWorkspaceInvalidCredentialsError(
@@ -294,7 +296,7 @@ class GoogleworkspaceProvider(Provider):
try:
credentials = service_account.Credentials.from_service_account_info(
credentials_data,
- scopes=GoogleworkspaceProvider.DIRECTORY_SCOPES,
+ scopes=GoogleworkspaceProvider.SCOPES,
)
except ValueError as error:
raise GoogleWorkspaceInvalidCredentialsError(
@@ -415,7 +417,7 @@ class GoogleworkspaceProvider(Provider):
)
# Fetch all domains (primary + aliases) to support domain aliases
- # The scope admin.directory.domain.readonly is already in DIRECTORY_SCOPES
+ # The scope admin.directory.domain.readonly is already in SCOPES above
try:
domains_response = service.domains().list(customer="my_customer").execute()
valid_domains = [
diff --git a/prowler/providers/googleworkspace/services/calendar/__init__.py b/prowler/providers/googleworkspace/services/calendar/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_client.py b/prowler/providers/googleworkspace/services/calendar/calendar_client.py
new file mode 100644
index 0000000000..9162bb3207
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_client.py
@@ -0,0 +1,6 @@
+from prowler.providers.common.provider import Provider
+from prowler.providers.googleworkspace.services.calendar.calendar_service import (
+ Calendar,
+)
+
+calendar_client = Calendar(Provider.get_global_provider())
diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/__init__.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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
new file mode 100644
index 0000000000..3a47f981d0
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json
@@ -0,0 +1,41 @@
+{
+ "Provider": "googleworkspace",
+ "CheckID": "calendar_external_invitations_warning",
+ "CheckTitle": "External invitation warnings are enabled for Google Calendar",
+ "CheckType": [],
+ "ServiceName": "calendar",
+ "SubServiceName": "",
+ "ResourceIdTemplate": "",
+ "Severity": "medium",
+ "ResourceType": "NotDefined",
+ "ResourceGroup": "collaboration",
+ "Description": "Google Calendar **warns users** when they invite guests from outside the organization to an event. This prompt gives users a chance to reconsider before sharing meeting details with external parties, reducing the likelihood of **accidental information disclosure** through calendar invitations.",
+ "Risk": "Without external invitation warnings, users may unintentionally include **external guests** in internal meetings, exposing **confidential meeting details**, agendas, and internal attendee lists to unauthorized parties. This is a common vector for inadvertent data leakage through everyday calendar actions.",
+ "RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://support.google.com/a/answer/6329284",
+ "https://knowledge.workspace.google.com/admin/calendar/set-google-calendar-sharing-options",
+ "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** > **Calendar**\n3. Click **Sharing settings**\n4. Under **External invitations**, check **Warn users when inviting guests outside of the domain**\n5. Click **Save**",
+ "Terraform": ""
+ },
+ "Recommendation": {
+ "Text": "Enable external invitation warnings so users are notified whenever a meeting invitation includes guests outside the organization. This simple prompt helps prevent accidental disclosure of meeting details to unintended recipients.",
+ "Url": "https://hub.prowler.com/check/calendar_external_invitations_warning"
+ }
+ },
+ "Categories": [
+ "internet-exposed"
+ ],
+ "DependsOn": [],
+ "RelatedTo": [
+ "calendar_external_sharing_primary_calendar",
+ "calendar_external_sharing_secondary_calendar"
+ ],
+ "Notes": ""
+}
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
new file mode 100644
index 0000000000..7af51cbfba
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py
@@ -0,0 +1,56 @@
+from typing import List
+
+from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
+from prowler.providers.googleworkspace.services.calendar.calendar_client import (
+ calendar_client,
+)
+
+
+class calendar_external_invitations_warning(Check):
+ """Check that external invitation warnings are enabled for Google Calendar
+
+ This check verifies that the domain-level policy warns users when they
+ invite guests from outside the organization, reducing the risk of accidental
+ information disclosure through calendar events.
+ """
+
+ def execute(self) -> List[CheckReportGoogleWorkspace]:
+ findings = []
+
+ if calendar_client.policies_fetched:
+ report = CheckReportGoogleWorkspace(
+ metadata=self.metadata(),
+ resource=calendar_client.provider.identity,
+ resource_name=calendar_client.provider.identity.domain,
+ resource_id=calendar_client.provider.identity.customer_id,
+ customer_id=calendar_client.provider.identity.customer_id,
+ location="global",
+ )
+
+ warning_enabled = calendar_client.policies.external_invitations_warning
+
+ if warning_enabled is True:
+ report.status = "PASS"
+ report.status_extended = (
+ f"External invitation warnings for Google Calendar are enabled "
+ f"in domain {calendar_client.provider.identity.domain}."
+ )
+ else:
+ report.status = "FAIL"
+ if warning_enabled is None:
+ report.status_extended = (
+ f"External invitation warnings for Google Calendar are not "
+ f"explicitly configured in domain "
+ f"{calendar_client.provider.identity.domain}. "
+ f"Users should be warned when inviting guests outside the organization."
+ )
+ else:
+ report.status_extended = (
+ f"External invitation warnings for Google Calendar are disabled "
+ f"in domain {calendar_client.provider.identity.domain}. "
+ f"Users should be warned when inviting guests outside the organization."
+ )
+
+ findings.append(report)
+
+ return findings
diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/__init__.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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
new file mode 100644
index 0000000000..536e8413f2
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json
@@ -0,0 +1,41 @@
+{
+ "Provider": "googleworkspace",
+ "CheckID": "calendar_external_sharing_primary_calendar",
+ "CheckTitle": "External sharing for primary calendars is restricted to free/busy only",
+ "CheckType": [],
+ "ServiceName": "calendar",
+ "SubServiceName": "",
+ "ResourceIdTemplate": "",
+ "Severity": "medium",
+ "ResourceType": "NotDefined",
+ "ResourceGroup": "collaboration",
+ "Description": "Primary calendars in the Google Workspace domain share **only free/busy information** with external users. When external sharing is set to share full event details, sensitive information such as meeting titles, attendees, locations, and descriptions is exposed to users outside the organization.",
+ "Risk": "Overly permissive external sharing of primary calendars exposes **sensitive meeting metadata** — titles, attendees, locations, and descriptions — to users outside the organization. This increases the risk of **information disclosure**, **social engineering**, and **targeted phishing** based on insights into organizational activities.",
+ "RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://support.google.com/a/answer/60765",
+ "https://knowledge.workspace.google.com/admin/calendar/set-google-calendar-sharing-options",
+ "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** > **Calendar**\n3. Click **Sharing settings**\n4. Under **External sharing options for primary calendars**, select **Only free/busy information (hide event details)**\n5. Click **Save**",
+ "Terraform": ""
+ },
+ "Recommendation": {
+ "Text": "Restrict external sharing of primary calendars to free/busy information only. This preserves scheduling functionality with external users while preventing exposure of sensitive meeting details.",
+ "Url": "https://hub.prowler.com/check/calendar_external_sharing_primary_calendar"
+ }
+ },
+ "Categories": [
+ "internet-exposed"
+ ],
+ "DependsOn": [],
+ "RelatedTo": [
+ "calendar_external_sharing_secondary_calendar",
+ "calendar_external_invitations_warning"
+ ],
+ "Notes": ""
+}
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
new file mode 100644
index 0000000000..b019acf4de
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py
@@ -0,0 +1,56 @@
+from typing import List
+
+from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
+from prowler.providers.googleworkspace.services.calendar.calendar_client import (
+ calendar_client,
+)
+
+
+class calendar_external_sharing_primary_calendar(Check):
+ """Check that external sharing for primary calendars is restricted to free/busy only
+
+ This check verifies that the domain-level policy for primary calendar external
+ sharing is set to share only free/busy information, preventing exposure of
+ event details to external users.
+ """
+
+ def execute(self) -> List[CheckReportGoogleWorkspace]:
+ findings = []
+
+ if calendar_client.policies_fetched:
+ report = CheckReportGoogleWorkspace(
+ metadata=self.metadata(),
+ resource=calendar_client.provider.identity,
+ resource_name=calendar_client.provider.identity.domain,
+ resource_id=calendar_client.provider.identity.customer_id,
+ customer_id=calendar_client.provider.identity.customer_id,
+ location="global",
+ )
+
+ sharing = calendar_client.policies.primary_calendar_external_sharing
+
+ if sharing == "EXTERNAL_FREE_BUSY_ONLY":
+ report.status = "PASS"
+ report.status_extended = (
+ f"Primary calendar external sharing in domain "
+ f"{calendar_client.provider.identity.domain} is restricted to "
+ f"free/busy information only."
+ )
+ else:
+ report.status = "FAIL"
+ if sharing is None:
+ report.status_extended = (
+ f"Primary calendar external sharing is not explicitly configured "
+ f"in domain {calendar_client.provider.identity.domain}. "
+ f"External sharing should be restricted to free/busy information only."
+ )
+ else:
+ report.status_extended = (
+ f"Primary calendar external sharing in domain "
+ f"{calendar_client.provider.identity.domain} is set to {sharing}. "
+ f"External sharing should be restricted to free/busy information only."
+ )
+
+ findings.append(report)
+
+ return findings
diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/__init__.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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
new file mode 100644
index 0000000000..1dadf4965c
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json
@@ -0,0 +1,41 @@
+{
+ "Provider": "googleworkspace",
+ "CheckID": "calendar_external_sharing_secondary_calendar",
+ "CheckTitle": "External sharing for secondary calendars is restricted to free/busy only",
+ "CheckType": [],
+ "ServiceName": "calendar",
+ "SubServiceName": "",
+ "ResourceIdTemplate": "",
+ "Severity": "medium",
+ "ResourceType": "NotDefined",
+ "ResourceGroup": "collaboration",
+ "Description": "Secondary calendars in the Google Workspace domain share **only free/busy information** with external users. Secondary calendars are additional calendars users create beyond their primary calendar (e.g., for projects, teams, or personal events), and are commonly used to organize sensitive or focused activities that should not be visible to external parties.",
+ "Risk": "Overly permissive external sharing of secondary calendars exposes **project-specific or team-specific event details** to users outside the organization. Because secondary calendars often hold more targeted activities (e.g., product launches, internal reviews), unrestricted external sharing increases the risk of **information disclosure** and **competitive intelligence leakage**.",
+ "RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://support.google.com/a/answer/60765",
+ "https://knowledge.workspace.google.com/admin/calendar/set-google-calendar-sharing-options",
+ "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** > **Calendar**\n3. Click **Sharing settings**\n4. Under **External sharing options for secondary calendars**, select **Only free/busy information (hide event details)**\n5. Click **Save**",
+ "Terraform": ""
+ },
+ "Recommendation": {
+ "Text": "Restrict external sharing of secondary calendars to free/busy information only. This preserves scheduling interoperability with external collaborators while preventing exposure of sensitive event details in user-created calendars.",
+ "Url": "https://hub.prowler.com/check/calendar_external_sharing_secondary_calendar"
+ }
+ },
+ "Categories": [
+ "internet-exposed"
+ ],
+ "DependsOn": [],
+ "RelatedTo": [
+ "calendar_external_sharing_primary_calendar",
+ "calendar_external_invitations_warning"
+ ],
+ "Notes": ""
+}
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
new file mode 100644
index 0000000000..f7a418b48e
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py
@@ -0,0 +1,56 @@
+from typing import List
+
+from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
+from prowler.providers.googleworkspace.services.calendar.calendar_client import (
+ calendar_client,
+)
+
+
+class calendar_external_sharing_secondary_calendar(Check):
+ """Check that external sharing for secondary calendars is restricted to free/busy only
+
+ This check verifies that the domain-level policy for secondary calendar external
+ sharing is set to share only free/busy information, preventing exposure of
+ event details in user-created calendars to external users.
+ """
+
+ def execute(self) -> List[CheckReportGoogleWorkspace]:
+ findings = []
+
+ if calendar_client.policies_fetched:
+ report = CheckReportGoogleWorkspace(
+ metadata=self.metadata(),
+ resource=calendar_client.provider.identity,
+ resource_name=calendar_client.provider.identity.domain,
+ resource_id=calendar_client.provider.identity.customer_id,
+ customer_id=calendar_client.provider.identity.customer_id,
+ location="global",
+ )
+
+ sharing = calendar_client.policies.secondary_calendar_external_sharing
+
+ if sharing == "EXTERNAL_FREE_BUSY_ONLY":
+ report.status = "PASS"
+ report.status_extended = (
+ f"Secondary calendar external sharing in domain "
+ f"{calendar_client.provider.identity.domain} is restricted to "
+ f"free/busy information only."
+ )
+ else:
+ report.status = "FAIL"
+ if sharing is None:
+ report.status_extended = (
+ f"Secondary calendar external sharing is not explicitly configured "
+ f"in domain {calendar_client.provider.identity.domain}. "
+ f"External sharing should be restricted to free/busy information only."
+ )
+ else:
+ report.status_extended = (
+ f"Secondary calendar external sharing in domain "
+ f"{calendar_client.provider.identity.domain} is set to {sharing}. "
+ f"External sharing should be restricted to free/busy information only."
+ )
+
+ findings.append(report)
+
+ return findings
diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_service.py b/prowler/providers/googleworkspace/services/calendar/calendar_service.py
new file mode 100644
index 0000000000..ae71c0fdf1
--- /dev/null
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_service.py
@@ -0,0 +1,112 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+from prowler.lib.logger import logger
+from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService
+
+
+class Calendar(GoogleWorkspaceService):
+ """Google Workspace Calendar service for auditing domain-level calendar policies.
+
+ Uses the Cloud Identity Policy API v1 to read calendar sharing
+ and invitation settings configured in the Admin Console.
+ """
+
+ def __init__(self, provider):
+ super().__init__(provider)
+ self.policies = CalendarPolicies()
+ self.policies_fetched = False
+ self._fetch_calendar_policies()
+
+ def _fetch_calendar_policies(self):
+ """Fetch calendar policies from the Cloud Identity Policy API v1."""
+ logger.info("Calendar - Fetching calendar 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)
+ fetch_succeeded = True
+
+ while request is not None:
+ try:
+ response = request.execute()
+
+ for policy in response.get("policies", []):
+ 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
+
+ self.policies_fetched = fetch_succeeded
+
+ logger.info(
+ f"Calendar policies fetched - "
+ f"Primary sharing: {self.policies.primary_calendar_external_sharing}, "
+ f"Secondary sharing: {self.policies.secondary_calendar_external_sharing}, "
+ f"Invitation warnings: {self.policies.external_invitations_warning}"
+ )
+
+ except Exception as error:
+ self._handle_api_error(
+ error,
+ "fetching calendar policies",
+ self.provider.identity.customer_id,
+ )
+ self.policies_fetched = False
+
+
+class CalendarPolicies(BaseModel):
+ """Model for domain-level Calendar policy settings."""
+
+ primary_calendar_external_sharing: Optional[str] = None
+ secondary_calendar_external_sharing: Optional[str] = None
+ external_invitations_warning: Optional[bool] = None
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
new file mode 100644
index 0000000000..f367d5938d
--- /dev/null
+++ b/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py
@@ -0,0 +1,130 @@
+from unittest.mock import patch
+
+from prowler.providers.googleworkspace.services.calendar.calendar_service import (
+ CalendarPolicies,
+)
+from tests.providers.googleworkspace.googleworkspace_fixtures import (
+ CUSTOMER_ID,
+ DOMAIN,
+ set_mocked_googleworkspace_provider,
+)
+
+
+class TestCalendarExternalInvitationsWarning:
+ def test_pass_warnings_enabled(self):
+ """Test PASS when external invitation warnings are 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.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning.calendar_client"
+ ) as mock_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning import (
+ calendar_external_invitations_warning,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ external_invitations_warning=True
+ )
+
+ check = calendar_external_invitations_warning()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "PASS"
+ assert "enabled" in findings[0].status_extended
+ assert findings[0].resource_name == DOMAIN
+ assert findings[0].customer_id == CUSTOMER_ID
+
+ def test_fail_warnings_disabled(self):
+ """Test FAIL when external invitation warnings are 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.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning.calendar_client"
+ ) as mock_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning import (
+ calendar_external_invitations_warning,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ external_invitations_warning=False
+ )
+
+ check = calendar_external_invitations_warning()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "FAIL"
+ assert "disabled" in findings[0].status_extended
+
+ def test_fail_no_policy_set(self):
+ """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning import (
+ calendar_external_invitations_warning,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ external_invitations_warning=None
+ )
+
+ check = calendar_external_invitations_warning()
+ 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.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning.calendar_client"
+ ) as mock_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_invitations_warning.calendar_external_invitations_warning import (
+ calendar_external_invitations_warning,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = False
+ mock_calendar_client.policies = CalendarPolicies()
+
+ check = calendar_external_invitations_warning()
+ findings = check.execute()
+
+ assert len(findings) == 0
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
new file mode 100644
index 0000000000..32056e9fcb
--- /dev/null
+++ b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py
@@ -0,0 +1,161 @@
+from unittest.mock import patch
+
+from prowler.providers.googleworkspace.services.calendar.calendar_service import (
+ CalendarPolicies,
+)
+from tests.providers.googleworkspace.googleworkspace_fixtures import (
+ CUSTOMER_ID,
+ DOMAIN,
+ set_mocked_googleworkspace_provider,
+)
+
+
+class TestCalendarExternalSharingPrimaryCalendar:
+ def test_pass_free_busy_only(self):
+ """Test PASS when external sharing is restricted to free/busy only"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar import (
+ calendar_external_sharing_primary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ primary_calendar_external_sharing="EXTERNAL_FREE_BUSY_ONLY"
+ )
+
+ check = calendar_external_sharing_primary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "PASS"
+ assert "free/busy information only" in findings[0].status_extended
+ assert findings[0].resource_name == DOMAIN
+ assert findings[0].customer_id == CUSTOMER_ID
+
+ def test_fail_read_only(self):
+ """Test FAIL when external sharing allows read-only access"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar import (
+ calendar_external_sharing_primary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ primary_calendar_external_sharing="EXTERNAL_ALL_INFO_READ_ONLY"
+ )
+
+ check = calendar_external_sharing_primary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "FAIL"
+ assert "EXTERNAL_ALL_INFO_READ_ONLY" in findings[0].status_extended
+ assert "free/busy information only" in findings[0].status_extended
+
+ def test_fail_read_write(self):
+ """Test FAIL when external sharing allows read-write access"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar import (
+ calendar_external_sharing_primary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ primary_calendar_external_sharing="EXTERNAL_ALL_INFO_READ_WRITE"
+ )
+
+ check = calendar_external_sharing_primary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "FAIL"
+ assert "EXTERNAL_ALL_INFO_READ_WRITE" in findings[0].status_extended
+
+ def test_fail_no_policy_set(self):
+ """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar import (
+ calendar_external_sharing_primary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ primary_calendar_external_sharing=None
+ )
+
+ check = calendar_external_sharing_primary_calendar()
+ 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.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar.calendar_client"
+ ) as mock_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_primary_calendar.calendar_external_sharing_primary_calendar import (
+ calendar_external_sharing_primary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = False
+ mock_calendar_client.policies = CalendarPolicies()
+
+ check = calendar_external_sharing_primary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 0
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
new file mode 100644
index 0000000000..800f9ab5f0
--- /dev/null
+++ b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py
@@ -0,0 +1,161 @@
+from unittest.mock import patch
+
+from prowler.providers.googleworkspace.services.calendar.calendar_service import (
+ CalendarPolicies,
+)
+from tests.providers.googleworkspace.googleworkspace_fixtures import (
+ CUSTOMER_ID,
+ DOMAIN,
+ set_mocked_googleworkspace_provider,
+)
+
+
+class TestCalendarExternalSharingSecondaryCalendar:
+ def test_pass_free_busy_only(self):
+ """Test PASS when external sharing is restricted to free/busy only"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar import (
+ calendar_external_sharing_secondary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ secondary_calendar_external_sharing="EXTERNAL_FREE_BUSY_ONLY"
+ )
+
+ check = calendar_external_sharing_secondary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "PASS"
+ assert "free/busy information only" in findings[0].status_extended
+ assert findings[0].resource_name == DOMAIN
+ assert findings[0].customer_id == CUSTOMER_ID
+
+ def test_fail_read_only(self):
+ """Test FAIL when external sharing allows read-only access"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar import (
+ calendar_external_sharing_secondary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ secondary_calendar_external_sharing="EXTERNAL_ALL_INFO_READ_ONLY"
+ )
+
+ check = calendar_external_sharing_secondary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "FAIL"
+ assert "EXTERNAL_ALL_INFO_READ_ONLY" in findings[0].status_extended
+ assert "free/busy information only" in findings[0].status_extended
+
+ def test_fail_read_write_manage(self):
+ """Test FAIL when external sharing allows read-write-manage access"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar import (
+ calendar_external_sharing_secondary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ secondary_calendar_external_sharing="EXTERNAL_ALL_INFO_READ_WRITE_MANAGE"
+ )
+
+ check = calendar_external_sharing_secondary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 1
+ assert findings[0].status == "FAIL"
+ assert "EXTERNAL_ALL_INFO_READ_WRITE_MANAGE" in findings[0].status_extended
+
+ def test_fail_no_policy_set(self):
+ """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ 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_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar import (
+ calendar_external_sharing_secondary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = True
+ mock_calendar_client.policies = CalendarPolicies(
+ secondary_calendar_external_sharing=None
+ )
+
+ check = calendar_external_sharing_secondary_calendar()
+ 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.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar.calendar_client"
+ ) as mock_calendar_client,
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_external_sharing_secondary_calendar.calendar_external_sharing_secondary_calendar import (
+ calendar_external_sharing_secondary_calendar,
+ )
+
+ mock_calendar_client.provider = mock_provider
+ mock_calendar_client.policies_fetched = False
+ mock_calendar_client.policies = CalendarPolicies()
+
+ check = calendar_external_sharing_secondary_calendar()
+ findings = check.execute()
+
+ assert len(findings) == 0
diff --git a/tests/providers/googleworkspace/services/calendar/calendar_service_test.py b/tests/providers/googleworkspace/services/calendar/calendar_service_test.py
new file mode 100644
index 0000000000..1491f839fb
--- /dev/null
+++ b/tests/providers/googleworkspace/services/calendar/calendar_service_test.py
@@ -0,0 +1,231 @@
+from unittest.mock import MagicMock, patch
+
+from tests.providers.googleworkspace.googleworkspace_fixtures import (
+ set_mocked_googleworkspace_provider,
+)
+
+
+class TestCalendarService:
+ def test_calendar_fetch_policies_all_settings(self):
+ """Test fetching all 3 calendar policy settings from Cloud Identity API"""
+ 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 the actual Cloud Identity Policy API v1 response shape:
+ # - "type" (not "name"), prefixed with "settings/"
+ # - inner value field names are camelCase
+ mock_policies_list.execute.return_value = {
+ "policies": [
+ {
+ "setting": {
+ "type": "settings/calendar.primary_calendar_max_allowed_external_sharing",
+ "value": {
+ "maxAllowedExternalSharing": "EXTERNAL_FREE_BUSY_ONLY"
+ },
+ }
+ },
+ {
+ "setting": {
+ "type": "settings/calendar.secondary_calendar_max_allowed_external_sharing",
+ "value": {
+ "maxAllowedExternalSharing": "EXTERNAL_ALL_INFO_READ_ONLY"
+ },
+ }
+ },
+ {
+ "setting": {
+ "type": "settings/calendar.external_invitations",
+ "value": {"warnOnInvite": 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.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.primary_calendar_external_sharing
+ == "EXTERNAL_FREE_BUSY_ONLY"
+ )
+ assert (
+ calendar.policies.secondary_calendar_external_sharing
+ == "EXTERNAL_ALL_INFO_READ_ONLY"
+ )
+ assert calendar.policies.external_invitations_warning is True
+
+ def test_calendar_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.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.primary_calendar_external_sharing is None
+ assert calendar.policies.secondary_calendar_external_sharing is None
+ assert calendar.policies.external_invitations_warning is None
+
+ def test_calendar_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.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 False
+ assert calendar.policies.primary_calendar_external_sharing is None
+ assert calendar.policies.secondary_calendar_external_sharing is None
+ assert calendar.policies.external_invitations_warning is None
+
+ def test_calendar_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.calendar.calendar_service.GoogleWorkspaceService._build_service",
+ return_value=None,
+ ),
+ ):
+ from prowler.providers.googleworkspace.services.calendar.calendar_service import (
+ Calendar,
+ )
+
+ calendar = Calendar(mock_provider)
+
+ assert calendar.policies_fetched is False
+ assert calendar.policies.primary_calendar_external_sharing is None
+ assert calendar.policies.secondary_calendar_external_sharing is None
+ assert calendar.policies.external_invitations_warning is None
+
+ def test_calendar_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.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 False
+ assert calendar.policies.primary_calendar_external_sharing is None
+ assert calendar.policies.secondary_calendar_external_sharing is None
+ assert calendar.policies.external_invitations_warning is None
+
+ def test_calendar_policies_model(self):
+ """Test CalendarPolicies Pydantic model"""
+ from prowler.providers.googleworkspace.services.calendar.calendar_service import (
+ CalendarPolicies,
+ )
+
+ policies = CalendarPolicies(
+ primary_calendar_external_sharing="EXTERNAL_FREE_BUSY_ONLY",
+ secondary_calendar_external_sharing="EXTERNAL_ALL_INFO_READ_WRITE",
+ external_invitations_warning=True,
+ )
+
+ assert policies.primary_calendar_external_sharing == "EXTERNAL_FREE_BUSY_ONLY"
+ assert (
+ policies.secondary_calendar_external_sharing
+ == "EXTERNAL_ALL_INFO_READ_WRITE"
+ )
+ assert policies.external_invitations_warning is True