feat(googleworkspace): add calendar service checks using Cloud Identity Policy API (#10597)

This commit is contained in:
lydiavilchez
2026-04-08 13:26:56 +02:00
committed by GitHub
parent 9290d7e105
commit bc38104903
21 changed files with 1150 additions and 22 deletions
@@ -6,17 +6,18 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.19.0" />
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 |
<Warning>
@@ -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**
<Note>
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.
</Note>
### 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)
+1
View File
@@ -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)
@@ -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",
@@ -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",
@@ -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 = [
@@ -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())
@@ -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": ""
}
@@ -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
@@ -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": ""
}
@@ -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
@@ -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": ""
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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