From b4befe3a108de885ca0e571bfa7b242df382c7c4 Mon Sep 17 00:00:00 2001 From: lydiavilchez <114735608+lydiavilchez@users.noreply.github.com> Date: Thu, 28 May 2026 10:15:10 +0200 Subject: [PATCH] feat(googleworkspace): add security service checks (#11356) Co-authored-by: pedrooot Co-authored-by: Hugo P.Brito --- .../googleworkspace/authentication.mdx | 8 +- prowler/CHANGELOG.md | 1 + .../cis_1.3_googleworkspace.json | 52 +- .../cisa_scuba_0.6_googleworkspace.json | 53 +- .../services/security/__init__.py | 0 .../security_2sv_enforced/__init__.py | 0 .../security_2sv_enforced.metadata.json | 39 ++ .../security_2sv_enforced.py | 59 +++ .../__init__.py | 0 ...ity_2sv_hardware_keys_admins.metadata.json | 39 ++ .../security_2sv_hardware_keys_admins.py | 64 +++ .../__init__.py | 0 ...vanced_protection_configured.metadata.json | 40 ++ ...security_advanced_protection_configured.py | 66 +++ .../__init__.py | 0 ...curity_app_access_restricted.metadata.json | 39 ++ .../security_app_access_restricted.py | 62 +++ .../services/security/security_client.py | 6 + .../__init__.py | 0 ...y_dlp_drive_rules_configured.metadata.json | 37 ++ .../security_dlp_drive_rules_configured.py | 50 ++ .../__init__.py | 0 ...curity_internal_apps_trusted.metadata.json | 39 ++ .../security_internal_apps_trusted.py | 56 ++ .../__init__.py | 0 ...ty_less_secure_apps_disabled.metadata.json | 37 ++ .../security_less_secure_apps_disabled.py | 56 ++ .../__init__.py | 0 ..._login_challenges_configured.metadata.json | 39 ++ .../security_login_challenges_configured.py | 62 +++ .../__init__.py | 0 ...urity_password_policy_strong.metadata.json | 37 ++ .../security_password_policy_strong.py | 76 +++ .../services/security/security_service.py | 281 ++++++++++ .../__init__.py | 0 ...ity_session_duration_limited.metadata.json | 37 ++ .../security_session_duration_limited.py | 75 +++ .../__init__.py | 0 ...uper_admin_recovery_disabled.metadata.json | 39 ++ .../security_super_admin_recovery_disabled.py | 55 ++ .../__init__.py | 0 ...curity_user_recovery_enabled.metadata.json | 39 ++ .../security_user_recovery_enabled.py | 56 ++ .../googleworkspace_security_service_test.py | 499 ++++++++++++++++++ .../security_2sv_enforced_test.py | 156 ++++++ .../security_2sv_hardware_keys_admins_test.py | 126 +++++ ...ity_advanced_protection_configured_test.py | 163 ++++++ .../security_app_access_restricted_test.py | 124 +++++ ...ecurity_dlp_drive_rules_configured_test.py | 124 +++++ .../security_internal_apps_trusted_test.py | 127 +++++ ...security_less_secure_apps_disabled_test.py | 127 +++++ ...curity_login_challenges_configured_test.py | 127 +++++ .../security_password_policy_strong_test.py | 210 ++++++++ .../security_session_duration_limited_test.py | 152 ++++++ ...rity_super_admin_recovery_disabled_test.py | 127 +++++ .../security_user_recovery_enabled_test.py | 124 +++++ 56 files changed, 3755 insertions(+), 30 deletions(-) create mode 100644 prowler/providers/googleworkspace/services/security/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_2sv_enforced/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.py create mode 100644 prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.py create mode 100644 prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.py create mode 100644 prowler/providers/googleworkspace/services/security/security_app_access_restricted/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.py create mode 100644 prowler/providers/googleworkspace/services/security/security_client.py create mode 100644 prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.py create mode 100644 prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.py create mode 100644 prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.py create mode 100644 prowler/providers/googleworkspace/services/security/security_login_challenges_configured/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.py create mode 100644 prowler/providers/googleworkspace/services/security/security_password_policy_strong/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.py create mode 100644 prowler/providers/googleworkspace/services/security/security_service.py create mode 100644 prowler/providers/googleworkspace/services/security/security_session_duration_limited/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.py create mode 100644 prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.py create mode 100644 prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.metadata.json create mode 100644 prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.py create mode 100644 tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled_test.py create mode 100644 tests/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled_test.py diff --git a/docs/user-guide/providers/googleworkspace/authentication.mdx b/docs/user-guide/providers/googleworkspace/authentication.mdx index efed268b9a..049b4fb4ae 100644 --- a/docs/user-guide/providers/googleworkspace/authentication.mdx +++ b/docs/user-guide/providers/googleworkspace/authentication.mdx @@ -18,7 +18,7 @@ Prowler requests the following read-only OAuth 2.0 scopes: | `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/admin.directory.orgunit.readonly` | Read access to organizational unit hierarchy (identifies the root OU for policy filtering) | -| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Gmail, Chat, and Drive service checks) | +| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Chat, Drive, Gmail, Groups, Marketplace, Security, and Sites service checks) | | `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments | @@ -40,7 +40,7 @@ In the [Google Cloud Console](https://console.cloud.google.com), select the targ | API | Required For | |-----|--------------| | **Admin SDK API** | Directory service checks (users, roles, domains) | -| **Cloud Identity API** | Calendar, Gmail, Chat, and Drive service checks (domain-level application policies) | +| **Cloud Identity API** | All service checks except Directory (domain-level application policies) | For each API: @@ -49,7 +49,7 @@ For each API: 3. Click **Enable** -Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar, Gmail, Chat, and Drive checks will return no findings if the Cloud Identity API is not enabled. +Both APIs must be enabled in the same GCP project that hosts the Service Account. All service checks except Directory will return no findings if the Cloud Identity API is not enabled. ### Step 3: Create a Service Account @@ -178,7 +178,7 @@ If Prowler connects but returns empty results or permission errors for specific ### Policy API Checks Return No Findings -If the Directory checks run successfully but the Calendar, Gmail, Chat, or Drive checks return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify: +If the Directory checks run successfully but other service checks (Calendar, Chat, Drive, Gmail, Groups, Marketplace, Security, Sites) 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) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 467f2d8ef0..ef37ca39b5 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `application` service for Okta provider with `application_admin_console_session_idle_timeout_15min`, `application_admin_console_mfa_required`, `application_admin_console_phishing_resistant_authentication`, `application_dashboard_mfa_required`, `application_dashboard_phishing_resistant_authentication`, and `application_authentication_policy_network_zone_enforced` checks [(#11358)](https://github.com/prowler-cloud/prowler/pull/11358) - AWS AI Security Framework compliance for AWS provider [(#11353)](https://github.com/prowler-cloud/prowler/pull/11353) - `storage_account_public_network_access_disabled` check for Azure provider and remapped the Azure CIS "Public Network Access is Disabled" requirements to it [(#11334)](https://github.com/prowler-cloud/prowler/pull/11334) +- 12 Security service checks for Google Workspace provider using the Cloud Identity Policy API [(#11356)](https://github.com/prowler-cloud/prowler/pull/11356) ### 🐞 Fixed diff --git a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json index 0867da103e..972cdfe7a2 100644 --- a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json @@ -1360,7 +1360,9 @@ { "Id": "4.1.1.1", "Description": "Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users in administrative roles", - "Checks": [], + "Checks": [ + "security_2sv_enforced" + ], "Attributes": [ { "Section": "4 Security", @@ -1381,7 +1383,9 @@ { "Id": "4.1.1.2", "Description": "Ensure hardware security keys are used for all users in administrative roles and other high-value accounts", - "Checks": [], + "Checks": [ + "security_2sv_hardware_keys_admins" + ], "Attributes": [ { "Section": "4 Security", @@ -1402,7 +1406,9 @@ { "Id": "4.1.1.3", "Description": "Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users", - "Checks": [], + "Checks": [ + "security_2sv_enforced" + ], "Attributes": [ { "Section": "4 Security", @@ -1423,7 +1429,9 @@ { "Id": "4.1.2.1", "Description": "Ensure Super Admin account recovery is disabled", - "Checks": [], + "Checks": [ + "security_super_admin_recovery_disabled" + ], "Attributes": [ { "Section": "4 Security", @@ -1444,7 +1452,9 @@ { "Id": "4.1.2.2", "Description": "Ensure User account recovery is enabled", - "Checks": [], + "Checks": [ + "security_user_recovery_enabled" + ], "Attributes": [ { "Section": "4 Security", @@ -1465,7 +1475,9 @@ { "Id": "4.1.3.1", "Description": "Ensure Advanced Protection Program is configured", - "Checks": [], + "Checks": [ + "security_advanced_protection_configured" + ], "Attributes": [ { "Section": "4 Security", @@ -1486,7 +1498,9 @@ { "Id": "4.1.4.1", "Description": "Ensure login challenges are enforced", - "Checks": [], + "Checks": [ + "security_login_challenges_configured" + ], "Attributes": [ { "Section": "4 Security", @@ -1507,7 +1521,9 @@ { "Id": "4.1.5.1", "Description": "Ensure password policy is configured for enhanced security", - "Checks": [], + "Checks": [ + "security_password_policy_strong" + ], "Attributes": [ { "Section": "4 Security", @@ -1528,7 +1544,9 @@ { "Id": "4.2.1.1", "Description": "Ensure application access to Google services is restricted", - "Checks": [], + "Checks": [ + "security_app_access_restricted" + ], "Attributes": [ { "Section": "4 Security", @@ -1570,7 +1588,9 @@ { "Id": "4.2.1.3", "Description": "Ensure internal apps can access Google Workspace APIs", - "Checks": [], + "Checks": [ + "security_internal_apps_trusted" + ], "Attributes": [ { "Section": "4 Security", @@ -1633,7 +1653,9 @@ { "Id": "4.2.3.1", "Description": "Ensure DLP policies for Google Drive are configured", - "Checks": [], + "Checks": [ + "security_dlp_drive_rules_configured" + ], "Attributes": [ { "Section": "4 Security", @@ -1654,7 +1676,9 @@ { "Id": "4.2.4.1", "Description": "Ensure Google session control is configured", - "Checks": [], + "Checks": [ + "security_session_duration_limited" + ], "Attributes": [ { "Section": "4 Security", @@ -1696,7 +1720,9 @@ { "Id": "4.2.6.1", "Description": "Ensure less secure app access is disabled", - "Checks": [], + "Checks": [ + "security_less_secure_apps_disabled" + ], "Attributes": [ { "Section": "4 Security", diff --git a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json index 0ce4d2ef6a..8fb840ae96 100644 --- a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json @@ -8,7 +8,10 @@ { "Id": "GWS.COMMONCONTROLS.1.1", "Description": "Phishing-resistant MFA SHALL be required for all users", - "Checks": [], + "Checks": [ + "security_2sv_enforced", + "security_2sv_hardware_keys_admins" + ], "Attributes": [ { "Section": "Common Controls", @@ -21,7 +24,9 @@ { "Id": "GWS.COMMONCONTROLS.1.2", "Description": "If phishing-resistant MFA is not yet tenable, an MFA method from the list of acceptable MFA methods SHALL be used as an interim solution", - "Checks": [], + "Checks": [ + "security_2sv_enforced" + ], "Attributes": [ { "Section": "Common Controls", @@ -112,7 +117,9 @@ { "Id": "GWS.COMMONCONTROLS.4.1", "Description": "Google Workspace sessions SHALL re-authenticate after 12 hours", - "Checks": [], + "Checks": [ + "security_session_duration_limited" + ], "Attributes": [ { "Section": "Common Controls", @@ -125,7 +132,9 @@ { "Id": "GWS.COMMONCONTROLS.5.1", "Description": "Password strength SHALL be enforced", - "Checks": [], + "Checks": [ + "security_password_policy_strong" + ], "Attributes": [ { "Section": "Common Controls", @@ -138,7 +147,9 @@ { "Id": "GWS.COMMONCONTROLS.5.2", "Description": "Minimum password length SHALL be at least 12 characters", - "Checks": [], + "Checks": [ + "security_password_policy_strong" + ], "Attributes": [ { "Section": "Common Controls", @@ -151,7 +162,9 @@ { "Id": "GWS.COMMONCONTROLS.5.3", "Description": "Minimum password length SHOULD be at least 15 characters", - "Checks": [], + "Checks": [ + "security_password_policy_strong" + ], "Attributes": [ { "Section": "Common Controls", @@ -164,7 +177,9 @@ { "Id": "GWS.COMMONCONTROLS.5.4", "Description": "Password policy SHALL be enforced at next sign-in", - "Checks": [], + "Checks": [ + "security_password_policy_strong" + ], "Attributes": [ { "Section": "Common Controls", @@ -177,7 +192,9 @@ { "Id": "GWS.COMMONCONTROLS.5.5", "Description": "Password reuse SHALL be restricted", - "Checks": [], + "Checks": [ + "security_password_policy_strong" + ], "Attributes": [ { "Section": "Common Controls", @@ -244,7 +261,9 @@ { "Id": "GWS.COMMONCONTROLS.8.1", "Description": "Account recovery for super admins SHALL be disabled", - "Checks": [], + "Checks": [ + "security_super_admin_recovery_disabled" + ], "Attributes": [ { "Section": "Common Controls", @@ -283,7 +302,9 @@ { "Id": "GWS.COMMONCONTROLS.9.1", "Description": "Privileged accounts SHALL be enrolled in the Advanced Protection Program", - "Checks": [], + "Checks": [ + "security_advanced_protection_configured" + ], "Attributes": [ { "Section": "Common Controls", @@ -296,7 +317,9 @@ { "Id": "GWS.COMMONCONTROLS.9.2", "Description": "Sensitive user accounts SHOULD be enrolled in the Advanced Protection Program", - "Checks": [], + "Checks": [ + "security_advanced_protection_configured" + ], "Attributes": [ { "Section": "Common Controls", @@ -361,7 +384,9 @@ { "Id": "GWS.COMMONCONTROLS.10.5", "Description": "Internal apps SHALL be allowed to access restricted Google Workspace APIs", - "Checks": [], + "Checks": [ + "security_internal_apps_trusted" + ], "Attributes": [ { "Section": "Common Controls", @@ -506,7 +531,9 @@ { "Id": "GWS.COMMONCONTROLS.18.1", "Description": "A DLP policy SHALL be configured for Drive", - "Checks": [], + "Checks": [ + "security_dlp_drive_rules_configured" + ], "Attributes": [ { "Section": "Common Controls", diff --git a/prowler/providers/googleworkspace/services/security/__init__.py b/prowler/providers/googleworkspace/services/security/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_2sv_enforced/__init__.py b/prowler/providers/googleworkspace/services/security/security_2sv_enforced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.metadata.json b/prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.metadata.json new file mode 100644 index 0000000000..659360aac5 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_2sv_enforced", + "CheckTitle": "2-Step Verification is enforced for all users", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level policy **enforces 2-Step Verification (Multi-Factor Authentication)** for all users. 2-Step Verification requires users to present a second form of authentication beyond their password, significantly reducing the risk of account compromise.", + "Risk": "Without 2-Step Verification enforcement, users can access their accounts with **only a password**. If credentials are compromised through phishing, credential stuffing, or data breaches, attackers gain **immediate access** to the user's account and organizational data without any additional verification.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/security/protect-your-business-with-2-step-verification", + "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 **Security** > **2-Step Verification**\n3. Check **Allow users to turn on 2-Step Verification**\n4. Set **Enforcement** to **On**\n5. Set **New user enrollment period** to **2 weeks**\n6. Under **Frequency**, uncheck **Allow user to trust device**\n7. Under **Methods**, select **Any except verification codes via text, phone call**\n8. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enforce **2-Step Verification** for all users to require a second authentication factor beyond passwords, protecting accounts from credential-based attacks.", + "Url": "https://hub.prowler.com/check/security_2sv_enforced" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "security_2sv_hardware_keys_admins" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.py b/prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.py new file mode 100644 index 0000000000..7cf17b348e --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced.py @@ -0,0 +1,59 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_2sv_enforced(Check): + """Check that 2-Step Verification is enforced for all users. + + This check verifies that the domain-level policy enforces 2-Step + Verification (Multi-Factor Authentication) for all users, reducing + the risk of account compromise through stolen credentials. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + enforced_from = security_client.policies.two_sv_enforced_from + # The API returns "1970-01-01T00:00:00Z" (protobuf zero-value + # Timestamp) when enforcement is OFF, not null or empty. + enforcement_off_epoch = "1970-01-01T00:00:00Z" + + if enforced_from and enforced_from != enforcement_off_epoch: + report.status = "PASS" + report.status_extended = ( + f"2-Step Verification enforcement is active " + f"(enforced from {enforced_from}) " + f"in domain {security_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if enforced_from is None: + report.status_extended = ( + f"2-Step Verification enforcement is not configured " + f"in domain {security_client.provider.identity.domain}. " + f"The default is OFF. 2-Step Verification should be " + f"enforced for all users." + ) + else: + report.status_extended = ( + f"2-Step Verification enforcement is set to OFF " + f"in domain {security_client.provider.identity.domain}. " + f"2-Step Verification should be enforced for all users." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/__init__.py b/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.metadata.json b/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.metadata.json new file mode 100644 index 0000000000..586cfc766a --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_2sv_hardware_keys_admins", + "CheckTitle": "Hardware security keys are required for 2-Step Verification", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level 2-Step Verification policy requires **hardware security keys only** as the allowed sign-in factor, providing the strongest phishing-resistant authentication. **Note**: the Policy API returns domain-wide policies only and cannot verify admin role-specific enforcement.", + "Risk": "When 2SV methods include **SMS, phone calls, or software-based authenticators**, users are vulnerable to **SIM swapping, SS7 attacks, and real-time phishing proxies** that can intercept one-time codes. Hardware security keys are resistant to all known remote phishing techniques.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/security/protect-your-business-with-2-step-verification", + "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 **Security** > **Authentication** > **2-Step Verification**\n3. Select the appropriate group with **ALL ADMIN ROLES** (create this group if needed)\n4. Under **Methods**, select **Only security key**\n5. Under **2-Step Verification policy suspension grace period**, select **1 day**\n6. Under **Security codes**, select **Don't allow users to generate security codes**\n7. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Require **hardware security keys only** for 2-Step Verification to provide the strongest phishing-resistant authentication for all users, particularly those in administrative roles.", + "Url": "https://hub.prowler.com/check/security_2sv_hardware_keys_admins" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "security_2sv_enforced" + ], + "Notes": "The Cloud Identity Policy API returns domain-wide policies only. It cannot verify that hardware keys are enforced specifically for admin roles versus all users. This check evaluates the customer-level enforcement factor, which applies to all users including administrators." +} diff --git a/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.py b/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.py new file mode 100644 index 0000000000..5913f448e1 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins.py @@ -0,0 +1,64 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_2sv_hardware_keys_admins(Check): + """Check that 2SV enforcement requires hardware security keys. + + This check verifies that the domain-level 2-Step Verification enforcement + factor is set to security keys only, providing the strongest protection + against phishing attacks. Note: the Cloud Identity Policy API returns + domain-wide policies — it cannot verify enforcement for admin roles + specifically. This check evaluates the customer-level policy which + applies to all users including administrators. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + factor_set = security_client.policies.two_sv_allowed_factor_set + + if factor_set == "PASSKEY_ONLY": + report.status = "PASS" + report.status_extended = ( + f"2-Step Verification enforcement requires security keys only " + f"in domain {security_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if factor_set is None: + report.status_extended = ( + f"2-Step Verification enforcement factor is not configured " + f"in domain {security_client.provider.identity.domain}. " + f"The default allows all methods including SMS and phone call. " + f"Security keys should be required for administrative accounts. " + f"Note: this check evaluates the domain-wide policy, the Policy " + f"API does not expose role-specific 2SV enforcement." + ) + else: + report.status_extended = ( + f"2-Step Verification enforcement factor is set to " + f"{factor_set} " + f"in domain {security_client.provider.identity.domain}. " + f"Only security keys (PASSKEY_ONLY) should be allowed for " + f"administrative accounts. " + f"Note: this check evaluates the domain-wide policy, the Policy " + f"API does not expose role-specific 2SV enforcement." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/__init__.py b/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.metadata.json b/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.metadata.json new file mode 100644 index 0000000000..6e9d72bfdf --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_advanced_protection_configured", + "CheckTitle": "Advanced Protection Program is configured", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level policy enables the **Advanced Protection Program** with user self-enrollment and **blocks the use of security codes**. The Advanced Protection Program is Google's strongest account security offering, requiring hardware security keys and applying a curated set of high-security policies.", + "Risk": "Without the Advanced Protection Program, user accounts rely on standard security controls that are vulnerable to **sophisticated phishing attacks, targeted credential theft, and third-party app data access**. Allowing security codes alongside Advanced Protection weakens its protections by providing an alternative authentication path that can be intercepted.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/security/protect-users-with-the-advanced-protection-program", + "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 **Security** > **Advanced Protection Program**\n3. Under **Enrollment**, select **Enable user enrollment**\n4. Under **Security Codes**, select **Do not allow users to generate security codes**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable the **Advanced Protection Program** with user self-enrollment and block **security codes** to enforce the strongest available account protection.", + "Url": "https://hub.prowler.com/check/security_advanced_protection_configured" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "security_2sv_enforced", + "security_2sv_hardware_keys_admins" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.py b/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.py new file mode 100644 index 0000000000..ab32837515 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured.py @@ -0,0 +1,66 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_advanced_protection_configured(Check): + """Check that the Advanced Protection Program is configured. + + This check verifies that the domain-level policy enables Advanced + Protection Program self-enrollment and blocks the use of security codes, + as recommended by CIS 4.1.3.1. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + enrollment = security_client.policies.advanced_protection_enrollment + code_option = ( + security_client.policies.advanced_protection_security_code_option + ) + domain = security_client.provider.identity.domain + + enrollment_ok = enrollment is True + codes_ok = code_option == "CODES_NOT_ALLOWED" + + if enrollment_ok and codes_ok: + report.status = "PASS" + report.status_extended = ( + f"Advanced Protection Program is configured with enrollment " + f"enabled and security codes blocked in domain {domain}." + ) + else: + report.status = "FAIL" + issues = [] + if not enrollment_ok: + issues.append( + "enrollment is not configured" + if enrollment is None + else "enrollment is disabled" + ) + if not codes_ok: + issues.append( + f"security codes are " + f"{code_option or 'using default (allowed without remote access)'} " + f"(should be CODES_NOT_ALLOWED)" + ) + report.status_extended = ( + f"Advanced Protection Program is not properly configured " + f"in domain {domain}: {'; '.join(issues)}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_app_access_restricted/__init__.py b/prowler/providers/googleworkspace/services/security/security_app_access_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.metadata.json b/prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.metadata.json new file mode 100644 index 0000000000..9a165141b6 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_app_access_restricted", + "CheckTitle": "Application access to Google services is restricted", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level API controls configuration **restricts third-party app access** to at least one Google service. This check verifies that the administrator has configured API access controls rather than leaving all services at the unrestricted default. The CIS benchmark recommends restricting access to all applicable services, particularly high-risk scopes like Drive and Gmail.", + "Risk": "When application access to Google services is unrestricted, **any third-party app** that users consent to can access sensitive organizational data through Google APIs. This includes apps that may request **broad OAuth scopes** for Drive, Gmail, and other services, potentially leading to **data exfiltration** through unvetted applications.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/apps/control-which-apps-access-google-workspace-data", + "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 **Security** > **Access and Data Control** > **API Controls**\n3. Click **App access control** > **MANAGE GOOGLE SERVICES**\n4. Select **ALL applicable Google Services**\n5. Click **Change access**\n6. Select **Restricted: Only trusted apps can access a service**\n7. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict **application access to Google services** to trusted apps only, particularly for high-risk scopes like **Drive and Gmail**, to prevent unvetted third-party apps from accessing sensitive organizational data.", + "Url": "https://hub.prowler.com/check/security_app_access_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "security_internal_apps_trusted" + ], + "Notes": "This check verifies that at least one Google service has API access restricted, serving as a signal that the administrator has configured API access controls. The CIS benchmark recommends restricting access to all applicable services." +} diff --git a/prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.py b/prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.py new file mode 100644 index 0000000000..4db6a95b83 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted.py @@ -0,0 +1,62 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_app_access_restricted(Check): + """Check that application access to Google services is restricted. + + This check verifies that at least one Google service has API access + restricted for third-party apps, indicating that the administrator + has reviewed and configured API access controls. The CIS benchmark + recommends restricting access to all applicable services, particularly + high-risk scopes like Drive and Gmail. This check serves as a signal + that API access controls have been configured rather than left at the + unrestricted default. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + restricted = security_client.policies.google_services_restricted + domain = security_client.provider.identity.domain + + if restricted is True: + report.status = "PASS" + report.status_extended = ( + f"Application access to Google services is restricted " + f"in domain {domain}. At least one Google service has " + f"API access limited to trusted apps." + ) + else: + report.status = "FAIL" + if restricted is None: + report.status_extended = ( + f"Application access to Google services is not configured " + f"in domain {domain}. The default is unrestricted. " + f"API access should be restricted for all applicable " + f"Google services, particularly high-risk scopes." + ) + else: + report.status_extended = ( + f"Application access to Google services is unrestricted " + f"in domain {domain}. " + f"API access should be restricted for all applicable " + f"Google services, particularly high-risk scopes." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_client.py b/prowler/providers/googleworkspace/services/security/security_client.py new file mode 100644 index 0000000000..5c8930edf1 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.security.security_service import ( + Security, +) + +security_client = Security(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/__init__.py b/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.metadata.json b/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.metadata.json new file mode 100644 index 0000000000..e25e45c395 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_dlp_drive_rules_configured", + "CheckTitle": "DLP policies for Google Drive are configured", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "At least one active **Data Loss Prevention (DLP) rule** targeting Google Drive file sharing is configured. DLP policies detect and prevent users from sharing sensitive information such as credit card numbers, identity numbers, and other regulated data through Drive.", + "Risk": "Without DLP policies, users can **freely share files containing sensitive information** through Google Drive without any detection or prevention controls. This increases the risk of **accidental data exposure, regulatory non-compliance**, and data breaches through oversharing.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/security/about-dlp", + "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 **Security** > **Access and Data Control** > **Data protection**\n3. Click **Manage Rules**\n4. Click **ADD RULE** and select **New rule** or **New rule from template**\n5. Set the rule name and scope\n6. Set triggers by checking **File modified** under **Google Drive**\n7. Add conditions (Field, Comparison Operator, Content to match)\n8. Under **Actions**, select the desired action for each incident\n9. Under **Alerting**, set severity and select **Send to alert center**\n10. Click **Create**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure **DLP policies for Google Drive** to detect and prevent sharing of sensitive information such as credit card numbers, identity numbers, and other regulated data.", + "Url": "https://hub.prowler.com/check/security_dlp_drive_rules_configured" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.py b/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.py new file mode 100644 index 0000000000..37304d0636 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured.py @@ -0,0 +1,50 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_dlp_drive_rules_configured(Check): + """Check that DLP policies for Google Drive are configured. + + This check verifies that at least one active Data Loss Prevention (DLP) + rule targeting Google Drive file sharing exists, helping to prevent + unintended exposure of sensitive information. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + dlp_exists = security_client.policies.dlp_drive_rules_exist + domain = security_client.provider.identity.domain + + if dlp_exists is True: + report.status = "PASS" + report.status_extended = ( + f"DLP policies for Google Drive are configured " + f"in domain {domain}. At least one active DLP rule " + f"targeting Drive file sharing exists." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"No active DLP policies for Google Drive are configured " + f"in domain {domain}. DLP rules should be configured " + f"to detect and prevent sharing of sensitive information " + f"through Drive." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/__init__.py b/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.metadata.json b/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.metadata.json new file mode 100644 index 0000000000..abf11509f4 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_internal_apps_trusted", + "CheckTitle": "Internal apps can access Google Workspace APIs", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level API controls configuration **trusts internal domain-owned apps** to access restricted Google Workspace APIs. This avoids the need to individually trust each internal app.", + "Risk": "When internal apps are not trusted, legitimate **organization-built applications** cannot access restricted Google Workspace API scopes, potentially **breaking internal workflows** and forcing administrators to trust each app individually, which increases administrative overhead.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/apps/control-which-apps-access-google-workspace-data", + "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 **Security** > **Access and Data Control** > **API Controls**\n3. Click **App access control** > **Settings**\n4. Check **Trust internal, domain-owned apps**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **trust for internal domain-owned apps** so organization-built applications can access restricted Google Workspace APIs without individual trust configuration.", + "Url": "https://hub.prowler.com/check/security_internal_apps_trusted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "security_app_access_restricted" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.py b/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.py new file mode 100644 index 0000000000..8a62124125 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted.py @@ -0,0 +1,56 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_internal_apps_trusted(Check): + """Check that internal apps can access Google Workspace APIs. + + This check verifies that the domain-level policy trusts internal + domain-owned apps, allowing them to access restricted Google Workspace + APIs without requiring individual trust configuration. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + trust_internal = security_client.policies.trust_internal_apps + + if trust_internal is True: + report.status = "PASS" + report.status_extended = ( + f"Internal domain-owned apps are trusted to access " + f"Google Workspace APIs " + f"in domain {security_client.provider.identity.domain}." + ) + elif trust_internal is None: + report.status = "PASS" + report.status_extended = ( + f"Internal domain-owned apps use Google's secure default " + f"configuration (trusted) " + f"in domain {security_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Internal domain-owned apps are not trusted to access " + f"Google Workspace APIs " + f"in domain {security_client.provider.identity.domain}. " + f"Internal apps should be trusted to access restricted APIs." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/__init__.py b/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.metadata.json b/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.metadata.json new file mode 100644 index 0000000000..ef4531b758 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_less_secure_apps_disabled", + "CheckTitle": "Less secure app access is disabled", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level policy **disables access to less secure apps** that do not use modern security standards such as OAuth. Blocking these apps helps keep user accounts and organizational data safe.", + "Risk": "When less secure app access is enabled, users can allow apps that use **basic authentication** (username and password only) to access their Google account. These apps are more vulnerable to **credential theft** and do not support 2-Step Verification, increasing the risk of account compromise.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/apps/control-access-to-less-secure-apps", + "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 **Security** > **Access and Data Control** > **Less secure apps**\n3. Select **Disable access to less secure apps (Recommended)**\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **less secure app access** to prevent users from allowing apps that do not use modern authentication standards to access their accounts.", + "Url": "https://hub.prowler.com/check/security_less_secure_apps_disabled" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.py b/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.py new file mode 100644 index 0000000000..a430f9d1e9 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled.py @@ -0,0 +1,56 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_less_secure_apps_disabled(Check): + """Check that less secure app access is disabled. + + This check verifies that the domain-level policy prevents users from + allowing access to apps that use less secure sign-in technology, + reducing the risk of credential compromise. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + less_secure_allowed = security_client.policies.less_secure_apps_allowed + + if less_secure_allowed is False: + report.status = "PASS" + report.status_extended = ( + f"Less secure app access is disabled " + f"in domain {security_client.provider.identity.domain}." + ) + elif less_secure_allowed is None: + report.status = "PASS" + report.status_extended = ( + f"Less secure app access uses Google's secure default " + f"configuration (disabled) " + f"in domain {security_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Less secure app access is enabled " + f"in domain {security_client.provider.identity.domain}. " + f"Less secure app access should be disabled to prevent " + f"credential compromise through apps that do not use modern " + f"security standards." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/__init__.py b/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.metadata.json b/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.metadata.json new file mode 100644 index 0000000000..0f4433ca78 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_login_challenges_configured", + "CheckTitle": "Login challenges are configured correctly", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level login challenges configuration has the **employee ID challenge disabled**. CIS 4.1.4.1 also requires Post-SSO verification to be enabled, but that setting is **not exposed by the Cloud Identity Policy API**. This check only covers the employee ID challenge portion of the control.", + "Risk": "When the employee ID login challenge is enabled without proper configuration, it may create a **false sense of security** or interfere with the login flow. The employee ID challenge is a supplementary verification method that should only be used when specifically required by the organization.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/security/protect-google-workspace-accounts-with-security-challenges", + "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 **Security** > **Login Challenges**\n3. Under **Login challenges**, uncheck **Use employee ID to keep my users more secure**\n4. Click **Save**\n5. Under **Post-SSO verification**, check **Logins using SSO are subject to additional verifications (if appropriate) and 2-Step Verification (if configured)** (this setting cannot be verified via the Policy API)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable the **employee ID login challenge** and manually verify that **Post-SSO verification** is enabled in the Admin Console, as the Post-SSO setting is not exposed by the Policy API.", + "Url": "https://hub.prowler.com/check/security_login_challenges_configured" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "security_2sv_enforced" + ], + "Notes": "This check is partial — it only verifies the employee ID challenge setting. CIS 4.1.4.1 also requires Post-SSO verification to be enabled, which is not exposed by the Cloud Identity Policy API and must be verified manually." +} diff --git a/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.py b/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.py new file mode 100644 index 0000000000..e3732053b8 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured.py @@ -0,0 +1,62 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_login_challenges_configured(Check): + """Check that login challenges are configured correctly. + + This check verifies that the employee ID login challenge is disabled, + as recommended by CIS. Note: CIS 4.1.4.1 also requires Post-SSO + verification to be enabled, but that setting is not exposed by the + Cloud Identity Policy API. This check only covers the employee ID + challenge portion of the control. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + employee_id_enabled = security_client.policies.login_challenge_employee_id + + if employee_id_enabled is False: + report.status = "PASS" + report.status_extended = ( + f"Employee ID login challenge is disabled " + f"in domain {security_client.provider.identity.domain}. " + f"Note: Post-SSO verification status cannot be verified " + f"via the Policy API." + ) + elif employee_id_enabled is None: + report.status = "PASS" + report.status_extended = ( + f"Employee ID login challenge uses Google's secure default " + f"configuration (disabled) " + f"in domain {security_client.provider.identity.domain}. " + f"Note: Post-SSO verification status cannot be verified " + f"via the Policy API." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Employee ID login challenge is enabled " + f"in domain {security_client.provider.identity.domain}. " + f"The employee ID challenge should be disabled per CIS " + f"recommendations. Note: Post-SSO verification status " + f"cannot be verified via the Policy API." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_password_policy_strong/__init__.py b/prowler/providers/googleworkspace/services/security/security_password_policy_strong/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.metadata.json b/prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.metadata.json new file mode 100644 index 0000000000..2bd351e93b --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_password_policy_strong", + "CheckTitle": "Password policy is configured for enhanced security", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level password policy is configured with **enhanced security settings**: minimum length of 14 characters, strong passwords enforced, password reuse disallowed, enforcement at next sign-in enabled, and password expiration set to 365 days.", + "Risk": "Weak password policies allow users to set **short, simple, or previously compromised passwords** that are vulnerable to brute-force attacks, credential stuffing, and password spraying. Without enforcement at sign-in, users may continue using weak passwords indefinitely.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/users/enforce-and-monitor-password-requirements-for-users", + "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 **Security** > **Password management**\n3. Under **Strength**, check **Enforce strong passwords**\n4. Under **Length**, set **Minimum Length** to **14** or greater\n5. Under **Strength and Length enforcement**, check **Enforce password policy at next sign-in**\n6. Under **Reuse**, uncheck **Allow password reuse**\n7. Under **Expiration**, set **Password reset frequency** to **365 Days**\n8. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure a **strong password policy** with minimum 14-character length, strong password enforcement, reuse prevention, enforcement at next sign-in, and annual password expiration.", + "Url": "https://hub.prowler.com/check/security_password_policy_strong" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.py b/prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.py new file mode 100644 index 0000000000..33448aa536 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong.py @@ -0,0 +1,76 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_password_policy_strong(Check): + """Check that password policy is configured for enhanced security. + + This check verifies that the domain-level password policy meets CIS + requirements: minimum length of 14 characters, strong passwords enforced, + password reuse disallowed, enforcement at next sign-in, and password + expiration configured. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + policies = security_client.policies + domain = security_client.provider.identity.domain + issues = [] + + min_length = policies.password_minimum_length + if min_length is None or min_length < 14: + issues.append( + "minimum length is not configured (requires 14+)" + if min_length is None + else f"minimum length is {min_length} (requires 14+)" + ) + + if policies.password_allowed_strength != "STRONG": + issues.append( + "password strength is not configured (requires STRONG)" + if policies.password_allowed_strength is None + else f"password strength is {policies.password_allowed_strength} (requires STRONG)" + ) + + if policies.password_allow_reuse is True: + issues.append("password reuse is allowed") + + if policies.password_enforce_at_login is not True: + issues.append("password policy is not enforced at next sign-in") + + expiration = policies.password_expiration_duration + if expiration is None or expiration == "0s": + issues.append("password expiration is not configured") + + if not issues: + report.status = "PASS" + report.status_extended = ( + f"Password policy meets CIS requirements " + f"in domain {domain}: minimum length {min_length}, " + f"strong passwords enforced, reuse disallowed, " + f"enforced at next sign-in, expiration configured." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Password policy does not meet CIS requirements " + f"in domain {domain}: {'; '.join(issues)}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_service.py b/prowler/providers/googleworkspace/services/security/security_service.py new file mode 100644 index 0000000000..96f24061f8 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_service.py @@ -0,0 +1,281 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Security(GoogleWorkspaceService): + """Google Workspace Security service for auditing domain-level security policies. + + Uses the Cloud Identity Policy API v1 to read authentication, password, + session, recovery, API control, and DLP settings configured in the + Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = SecurityPolicies() + self.policies_fetched = False + self._fetch_security_policies() + + def _fetch_security_policies(self): + """Fetch security policies from the Cloud Identity Policy API v1.""" + logger.info("Security - Fetching security policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + fetch_succeeded = True + + # Fetch 1: security.* settings + fetch_succeeded = self._fetch_namespace( + service, 'setting.type.matches("security.*")', fetch_succeeded + ) + + # Fetch 2: api_controls.* settings + fetch_succeeded = self._fetch_namespace( + service, 'setting.type.matches("api_controls.*")', fetch_succeeded + ) + + # Fetch 3: rule.dlp for DLP existence check + fetch_succeeded = self._fetch_namespace( + service, 'setting.type.matches("rule.dlp")', fetch_succeeded + ) + + self.policies_fetched = fetch_succeeded + + if fetch_succeeded: + logger.info("Security policies fetched successfully.") + else: + logger.warning( + "Security policies fetched with partial failures; " + "some checks may be skipped." + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching security policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + def _fetch_namespace(self, service, filter_str: str, fetch_succeeded: bool) -> bool: + """Fetch policies for a single namespace filter.""" + try: + request = service.policies().list( + pageSize=100, + filter=filter_str, + ) + + while request is not None: + try: + response = request.execute() + + for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue + + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) + + self._process_setting(setting_type, value) + + request = service.policies().list_next(request, response) + + except Exception as error: + self._handle_api_error( + error, + f"fetching policies with filter {filter_str}", + self.provider.identity.customer_id, + ) + return False + + except Exception as error: + self._handle_api_error( + error, + f"listing policies with filter {filter_str}", + self.provider.identity.customer_id, + ) + return False + + return fetch_succeeded + + def _process_setting(self, setting_type: str, value: dict): + """Process a single policy setting and populate the model.""" + + # 2-Step Verification settings + if setting_type == "security.two_step_verification_enrollment": + self.policies.two_sv_allow_enrollment = value.get("allowEnrollment") + logger.debug(f"2SV enrollment: {self.policies.two_sv_allow_enrollment}") + + elif setting_type == "security.two_step_verification_enforcement": + self.policies.two_sv_enforced_from = value.get("enforcedFrom") + logger.debug(f"2SV enforcement: {self.policies.two_sv_enforced_from}") + + elif setting_type == "security.two_step_verification_enforcement_factor": + self.policies.two_sv_allowed_factor_set = value.get( + "allowedSignInFactorSet" + ) + logger.debug(f"2SV factor set: {self.policies.two_sv_allowed_factor_set}") + + elif setting_type == "security.two_step_verification_device_trust": + self.policies.two_sv_allow_trusting_device = value.get( + "allowTrustingDevice" + ) + logger.debug( + f"2SV device trust: {self.policies.two_sv_allow_trusting_device}" + ) + + elif setting_type == "security.two_step_verification_grace_period": + self.policies.two_sv_enrollment_grace_period = value.get( + "enrollmentGracePeriod" + ) + logger.debug( + f"2SV grace period: {self.policies.two_sv_enrollment_grace_period}" + ) + + elif setting_type == "security.two_step_verification_sign_in_code": + self.policies.two_sv_backup_code_exception_period = value.get( + "backupCodeExceptionPeriod" + ) + logger.debug( + f"2SV backup code period: {self.policies.two_sv_backup_code_exception_period}" + ) + + # Account recovery + elif setting_type == "security.super_admin_account_recovery": + self.policies.super_admin_recovery_enabled = value.get( + "enableAccountRecovery" + ) + logger.debug( + f"Super admin recovery: {self.policies.super_admin_recovery_enabled}" + ) + + elif setting_type == "security.user_account_recovery": + self.policies.user_recovery_enabled = value.get("enableAccountRecovery") + logger.debug(f"User recovery: {self.policies.user_recovery_enabled}") + + # Advanced Protection Program + elif setting_type == "security.advanced_protection_program": + self.policies.advanced_protection_enrollment = value.get( + "enableAdvancedProtectionSelfEnrollment" + ) + self.policies.advanced_protection_security_code_option = value.get( + "securityCodeOption" + ) + logger.debug("Advanced Protection Program settings fetched.") + + # Login challenges + elif setting_type == "security.login_challenges": + self.policies.login_challenge_employee_id = value.get( + "enableEmployeeIdChallenge" + ) + logger.debug("Login challenges settings fetched.") + + # Password policy + elif setting_type == "security.password": + self.policies.password_minimum_length = value.get("minimumLength") + self.policies.password_maximum_length = value.get("maximumLength") + self.policies.password_allowed_strength = value.get("allowedStrength") + self.policies.password_allow_reuse = value.get("allowReuse") + self.policies.password_enforce_at_login = value.get( + "enforceRequirementsAtLogin" + ) + self.policies.password_expiration_duration = value.get("expirationDuration") + logger.debug("Password policy settings fetched.") + + # Less secure apps + elif setting_type == "security.less_secure_apps": + self.policies.less_secure_apps_allowed = value.get("allowLessSecureApps") + logger.debug(f"Less secure apps: {self.policies.less_secure_apps_allowed}") + + # Session controls + elif setting_type == "security.session_controls": + self.policies.web_session_duration = value.get("webSessionDuration") + logger.debug(f"Web session duration: {self.policies.web_session_duration}") + + # Passkeys restriction + elif setting_type == "security.passkeys_restriction": + self.policies.passkeys_type = value.get("allowedPasskeysType") + logger.debug(f"Passkeys type: {self.policies.passkeys_type}") + + # API controls - internal apps + elif setting_type == "api_controls.internal_apps": + self.policies.trust_internal_apps = value.get("trustInternalApps") + logger.debug(f"Trust internal apps: {self.policies.trust_internal_apps}") + + # API controls - google services + elif setting_type == "api_controls.google_services": + services = value.get("services", []) + for svc in services: + if svc.get("isEnabled") is False: + self.policies.google_services_restricted = True + break + if self.policies.google_services_restricted is None: + self.policies.google_services_restricted = False + logger.debug( + f"Google services restricted: {self.policies.google_services_restricted}" + ) + + # DLP rules + elif setting_type == "rule.dlp": + state = value.get("state") + triggers = value.get("triggers", []) + if state == "ACTIVE" and any( + trigger.startswith("google.workspace.drive.") for trigger in triggers + ): + self.policies.dlp_drive_rules_exist = True + logger.debug(f"DLP rule: state={state}, triggers={triggers}") + + +class SecurityPolicies(BaseModel): + """Model for domain-level Security policy settings.""" + + # security.two_step_verification_enrollment + two_sv_allow_enrollment: Optional[bool] = None + # security.two_step_verification_enforcement + two_sv_enforced_from: Optional[str] = None + # security.two_step_verification_enforcement_factor + two_sv_allowed_factor_set: Optional[str] = None + # security.two_step_verification_device_trust + two_sv_allow_trusting_device: Optional[bool] = None + # security.two_step_verification_grace_period + two_sv_enrollment_grace_period: Optional[str] = None + # security.two_step_verification_sign_in_code + two_sv_backup_code_exception_period: Optional[str] = None + # security.super_admin_account_recovery + super_admin_recovery_enabled: Optional[bool] = None + # security.user_account_recovery + user_recovery_enabled: Optional[bool] = None + # security.advanced_protection_program + advanced_protection_enrollment: Optional[bool] = None + advanced_protection_security_code_option: Optional[str] = None + # security.login_challenges + login_challenge_employee_id: Optional[bool] = None + # security.password + password_minimum_length: Optional[int] = None + password_maximum_length: Optional[int] = None + password_allowed_strength: Optional[str] = None + password_allow_reuse: Optional[bool] = None + password_enforce_at_login: Optional[bool] = None + password_expiration_duration: Optional[str] = None + # security.less_secure_apps + less_secure_apps_allowed: Optional[bool] = None + # security.session_controls + web_session_duration: Optional[str] = None + # security.passkeys_restriction + passkeys_type: Optional[str] = None + # api_controls.internal_apps + trust_internal_apps: Optional[bool] = None + # api_controls.google_services + google_services_restricted: Optional[bool] = None + # rule.dlp + dlp_drive_rules_exist: Optional[bool] = None diff --git a/prowler/providers/googleworkspace/services/security/security_session_duration_limited/__init__.py b/prowler/providers/googleworkspace/services/security/security_session_duration_limited/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.metadata.json b/prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.metadata.json new file mode 100644 index 0000000000..5cc4916945 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_session_duration_limited", + "CheckTitle": "Google session control is configured to 12 hours or less", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level Google session control limits web session duration to **12 hours or less**. When a session expires, users must re-authenticate, reducing the window of opportunity for session hijacking.", + "Risk": "The default 14-day session duration means that a compromised session token provides an attacker with **two weeks of uninterrupted access** without re-authentication. Shorter session durations limit the impact of **stolen cookies, session hijacking, and unauthorized access** from shared or untrusted devices.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/security/set-session-length-for-google-services", + "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 **Security** > **Access and Data Control** > **Google session control**\n3. Set **Web session duration** to **12 hours** or less\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Set **Google session control** web session duration to **12 hours or less** to limit the window of access from compromised sessions.", + "Url": "https://hub.prowler.com/check/security_session_duration_limited" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.py b/prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.py new file mode 100644 index 0000000000..1b72310ee6 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited.py @@ -0,0 +1,75 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + +MAX_SESSION_DURATION_SECONDS = 43200 # 12 hours + + +class security_session_duration_limited(Check): + """Check that Google session control is configured to 12 hours or less. + + This check verifies that the domain-level web session duration is set + to 12 hours or less, requiring users to re-authenticate more frequently + than the default 14-day session length. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + duration_str = security_client.policies.web_session_duration + domain = security_client.provider.identity.domain + + if duration_str is None: + report.status = "FAIL" + report.status_extended = ( + f"Google session control is not explicitly configured " + f"in domain {domain}. The default is 14 days. " + f"Web session duration should be 12 hours or less." + ) + findings.append(report) + return findings + + try: + duration_seconds = int(duration_str.removesuffix("s")) + except ValueError: + logger.error(f"Unparseable web session duration: {duration_str!r}") + report.status = "FAIL" + report.status_extended = ( + f"Web session duration value {duration_str!r} is not parseable " + f"in domain {domain}." + ) + findings.append(report) + return findings + + duration_hours = duration_seconds / 3600 + + if duration_seconds <= MAX_SESSION_DURATION_SECONDS: + report.status = "PASS" + report.status_extended = ( + f"Google session control is set to {duration_hours:.0f} hours " + f"in domain {domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Google session control is set to {duration_hours:.0f} hours " + f"in domain {domain}. " + f"Web session duration should be 12 hours or less." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/__init__.py b/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.metadata.json b/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.metadata.json new file mode 100644 index 0000000000..c39fb69d03 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_super_admin_recovery_disabled", + "CheckTitle": "Super Admin account recovery is disabled", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level policy **disables self-service account recovery for Super Admin accounts**. When disabled, Super Admins cannot use recovery options (phone, email) to regain access, reducing the risk of account takeover through compromised recovery channels.", + "Risk": "If Super Admin account recovery is enabled, an attacker who compromises a Super Admin's **recovery phone or email** could use the self-service recovery flow to take over the account, gaining **full administrative control** over the entire Google Workspace organization.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/users/allow-super-administrators-to-recover-their-password", + "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 **Security** > **Authentication** > **Account recovery**\n3. Select **Super admin account recovery**\n4. Uncheck **Allow super admins to recover their account**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **Super Admin account recovery** to prevent attackers from exploiting the self-service recovery flow to take over privileged accounts.", + "Url": "https://hub.prowler.com/check/security_super_admin_recovery_disabled" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "security_user_recovery_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.py b/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.py new file mode 100644 index 0000000000..5c201e7b5d --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled.py @@ -0,0 +1,55 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_super_admin_recovery_disabled(Check): + """Check that Super Admin account recovery is disabled. + + This check verifies that the domain-level policy prevents Super Admin + users from recovering their account through self-service, reducing the + risk of account takeover through the recovery flow. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + recovery_enabled = security_client.policies.super_admin_recovery_enabled + + if recovery_enabled is False: + report.status = "PASS" + report.status_extended = ( + f"Super Admin account recovery is disabled " + f"in domain {security_client.provider.identity.domain}." + ) + elif recovery_enabled is None: + report.status = "PASS" + report.status_extended = ( + f"Super Admin account recovery uses Google's secure default " + f"configuration (disabled) " + f"in domain {security_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Super Admin account recovery is enabled " + f"in domain {security_client.provider.identity.domain}. " + f"Super Admin account recovery should be disabled to prevent " + f"account takeover through the recovery flow." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/__init__.py b/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.metadata.json b/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.metadata.json new file mode 100644 index 0000000000..4d3e7dcb64 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "security_user_recovery_enabled", + "CheckTitle": "User account recovery is enabled", + "CheckType": [], + "ServiceName": "security", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The domain-level policy **enables self-service account recovery for non-Super Admin users**. When enabled, users can recover access to their accounts if their password is forgotten, reducing helpdesk burden and downtime.", + "Risk": "When user account recovery is disabled, users who lose access to their accounts must **contact an administrator** to regain access. This increases helpdesk burden, extends downtime, and may lead to users adopting **insecure workarounds** like sharing credentials.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://knowledge.workspace.google.com/admin/users/set-up-password-recovery-for-users", + "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 **Security** > **Authentication** > **Account recovery**\n3. Select **User account recovery**\n4. Check **Allow users and non-super admins to recover their account**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **user account recovery** to allow non-admin users to regain access to their accounts through self-service, reducing helpdesk burden and user downtime.", + "Url": "https://hub.prowler.com/check/security_user_recovery_enabled" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "security_super_admin_recovery_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.py b/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.py new file mode 100644 index 0000000000..99aabcb842 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled.py @@ -0,0 +1,56 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.security_client import ( + security_client, +) + + +class security_user_recovery_enabled(Check): + """Check that user account recovery is enabled. + + This check verifies that the domain-level policy allows non-Super Admin + users to recover their accounts through self-service, reducing helpdesk + burden while maintaining access to their accounts. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if security_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=security_client.policies, + resource_id="securityPolicies", + resource_name="Security Policies", + customer_id=security_client.provider.identity.customer_id, + ) + + recovery_enabled = security_client.policies.user_recovery_enabled + + if recovery_enabled is True: + report.status = "PASS" + report.status_extended = ( + f"User account recovery is enabled " + f"in domain {security_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if recovery_enabled is None: + report.status_extended = ( + f"User account recovery is not explicitly configured " + f"in domain {security_client.provider.identity.domain}. " + f"The default is disabled. User account recovery should be " + f"enabled to reduce helpdesk burden." + ) + else: + report.status_extended = ( + f"User account recovery is disabled " + f"in domain {security_client.provider.identity.domain}. " + f"User account recovery should be enabled to reduce " + f"helpdesk burden." + ) + + findings.append(report) + + return findings diff --git a/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py b/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py new file mode 100644 index 0000000000..13335e22cb --- /dev/null +++ b/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py @@ -0,0 +1,499 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestSecurityService: + def test_fetch_policies_all_security_settings(self): + """Test fetching security policies 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() + + # First call: security.* settings + mock_security_list = MagicMock() + mock_security_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/security.two_step_verification_enrollment", + "value": { + "allowEnrollment": True, + }, + } + }, + { + "setting": { + "type": "settings/security.two_step_verification_enforcement", + "value": { + "enforcedFrom": "2026-05-25T15:27:52.352Z", + }, + } + }, + { + "setting": { + "type": "settings/security.two_step_verification_enforcement_factor", + "value": { + "allowedSignInFactorSet": "ALL", + }, + } + }, + { + "setting": { + "type": "settings/security.two_step_verification_device_trust", + "value": { + "allowTrustingDevice": True, + }, + } + }, + { + "setting": { + "type": "settings/security.two_step_verification_grace_period", + "value": { + "enrollmentGracePeriod": "0s", + }, + } + }, + { + "setting": { + "type": "settings/security.two_step_verification_sign_in_code", + "value": { + "backupCodeExceptionPeriod": "86400s", + }, + } + }, + { + "setting": { + "type": "settings/security.super_admin_account_recovery", + "value": { + "enableAccountRecovery": True, + }, + } + }, + { + "setting": { + "type": "settings/security.user_account_recovery", + "value": { + "enableAccountRecovery": False, + }, + } + }, + { + "setting": { + "type": "settings/security.password", + "value": { + "allowedStrength": "STRONG", + "minimumLength": 8, + "maximumLength": 100, + "enforceRequirementsAtLogin": False, + "allowReuse": False, + "expirationDuration": "0s", + }, + } + }, + { + "setting": { + "type": "settings/security.session_controls", + "value": { + "webSessionDuration": "1209600s", + }, + } + }, + { + "setting": { + "type": "settings/security.less_secure_apps", + "value": { + "allowLessSecureApps": False, + }, + } + }, + { + "setting": { + "type": "settings/security.advanced_protection_program", + "value": { + "enableAdvancedProtectionSelfEnrollment": True, + "securityCodeOption": "CODES_NOT_ALLOWED", + }, + } + }, + { + "setting": { + "type": "settings/security.login_challenges", + "value": { + "enableEmployeeIdChallenge": False, + }, + } + }, + { + "setting": { + "type": "settings/security.passkeys_restriction", + "value": { + "allowedPasskeysType": "ANY_DEVICE_OR_PLATFORM", + }, + } + }, + ] + } + + # Second call: api_controls.* settings + mock_api_controls_list = MagicMock() + mock_api_controls_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/api_controls.internal_apps", + "value": { + "trustInternalApps": True, + }, + } + }, + { + "setting": { + "type": "settings/api_controls.google_services", + "value": { + "services": [ + {"scopesGroup": "DRIVE_ALL", "isEnabled": True}, + {"scopesGroup": "GMAIL_HIGH_RISK", "isEnabled": False}, + ], + }, + } + }, + ] + } + + # Third call: rule.dlp + mock_dlp_list = MagicMock() + mock_dlp_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/rule.dlp", + "value": { + "displayName": "PII Detection", + "triggers": ["google.workspace.drive.file.v1.share"], + "state": "ACTIVE", + }, + } + }, + ] + } + + mock_service.policies().list.side_effect = [ + mock_security_list, + mock_api_controls_list, + mock_dlp_list, + ] + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is True + assert security.policies.two_sv_allow_enrollment is True + assert security.policies.two_sv_enforced_from == "2026-05-25T15:27:52.352Z" + assert security.policies.two_sv_allowed_factor_set == "ALL" + assert security.policies.two_sv_allow_trusting_device is True + assert security.policies.two_sv_enrollment_grace_period == "0s" + assert security.policies.two_sv_backup_code_exception_period == "86400s" + assert security.policies.super_admin_recovery_enabled is True + assert security.policies.user_recovery_enabled is False + assert security.policies.password_minimum_length == 8 + assert security.policies.password_allowed_strength == "STRONG" + assert security.policies.password_allow_reuse is False + assert security.policies.password_enforce_at_login is False + assert security.policies.password_expiration_duration == "0s" + assert security.policies.web_session_duration == "1209600s" + assert security.policies.less_secure_apps_allowed is False + assert security.policies.advanced_protection_enrollment is True + assert ( + security.policies.advanced_protection_security_code_option + == "CODES_NOT_ALLOWED" + ) + assert security.policies.login_challenge_employee_id is False + assert security.policies.passkeys_type == "ANY_DEVICE_OR_PLATFORM" + assert security.policies.trust_internal_apps is True + assert security.policies.google_services_restricted is True + assert security.policies.dlp_drive_rules_exist is True + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response across all namespaces""" + 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_empty = MagicMock() + mock_empty.execute.return_value = {"policies": []} + mock_service.policies().list.side_effect = [ + mock_empty, + mock_empty, + mock_empty, + ] + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is True + assert security.policies.two_sv_enforced_from is None + assert security.policies.super_admin_recovery_enabled is None + assert security.policies.password_minimum_length is None + assert security.policies.web_session_duration is None + assert security.policies.less_secure_apps_allowed is None + assert security.policies.trust_internal_apps is None + assert security.policies.dlp_drive_rules_exist is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_service.policies().list.side_effect = Exception("API Error") + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is False + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails""" + 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.security.security_service.GoogleWorkspaceService._build_service", + return_value=None, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is False + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is False + + def test_fetch_policies_google_services_no_restricted(self): + """Test google_services with all services enabled sets restricted to False""" + 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_empty = MagicMock() + mock_empty.execute.return_value = {"policies": []} + + mock_api_list = MagicMock() + mock_api_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/api_controls.google_services", + "value": { + "services": [ + {"scopesGroup": "DRIVE_ALL", "isEnabled": True}, + {"scopesGroup": "GMAIL_ALL", "isEnabled": True}, + ], + }, + } + }, + ] + } + + mock_service.policies().list.side_effect = [ + mock_empty, + mock_api_list, + mock_empty, + ] + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is True + assert security.policies.google_services_restricted is False + + def test_dlp_rule_without_drive_trigger_ignored(self): + """Test that DLP rules without Drive triggers don't set dlp_drive_rules_exist""" + 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_empty = MagicMock() + mock_empty.execute.return_value = {"policies": []} + + mock_dlp_list = MagicMock() + mock_dlp_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/rule.dlp", + "value": { + "displayName": "Gmail Only Rule", + "triggers": ["google.workspace.gmail.email.v1.send"], + "state": "ACTIVE", + }, + } + }, + ] + } + + mock_service.policies().list.side_effect = [ + mock_empty, + mock_empty, + mock_dlp_list, + ] + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.security.security_service import ( + Security, + ) + + security = Security(mock_provider) + + assert security.policies_fetched is True + assert security.policies.dlp_drive_rules_exist is None + + def test_security_policies_model(self): + """Test SecurityPolicies Pydantic model""" + from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, + ) + + policies = SecurityPolicies( + two_sv_enforced_from="2026-05-25T15:27:52.352Z", + super_admin_recovery_enabled=False, + password_minimum_length=14, + web_session_duration="43200s", + ) + + assert policies.two_sv_enforced_from == "2026-05-25T15:27:52.352Z" + assert policies.super_admin_recovery_enabled is False + assert policies.password_minimum_length == 14 + assert policies.web_session_duration == "43200s" diff --git a/tests/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced_test.py b/tests/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced_test.py new file mode 100644 index 0000000000..a6d6a549a9 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_2sv_enforced/security_2sv_enforced_test.py @@ -0,0 +1,156 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurity2svEnforced: + def test_pass_2sv_enforced(self): + """Test PASS when 2-Step Verification enforcement is active""" + 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.security.security_2sv_enforced.security_2sv_enforced.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_enforced.security_2sv_enforced import ( + security_2sv_enforced, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + two_sv_enforced_from="2026-05-25T15:27:52.352Z" + ) + + check = security_2sv_enforced() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "active" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_none_not_configured(self): + """Test FAIL when 2-Step Verification enforcement is not configured (None)""" + 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.security.security_2sv_enforced.security_2sv_enforced.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_enforced.security_2sv_enforced import ( + security_2sv_enforced, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(two_sv_enforced_from=None) + + check = security_2sv_enforced() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not configured" in findings[0].status_extended + + def test_fail_empty_off(self): + """Test FAIL when 2-Step Verification enforcement is set to OFF (empty string)""" + 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.security.security_2sv_enforced.security_2sv_enforced.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_enforced.security_2sv_enforced import ( + security_2sv_enforced, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(two_sv_enforced_from="") + + check = security_2sv_enforced() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "OFF" in findings[0].status_extended + + def test_fail_epoch_enforcement_off(self): + """Test FAIL when API returns epoch zero timestamp (enforcement OFF)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_2sv_enforced.security_2sv_enforced.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_enforced.security_2sv_enforced import ( + security_2sv_enforced, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + two_sv_enforced_from="1970-01-01T00:00:00Z" + ) + + check = security_2sv_enforced() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "OFF" 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.security.security_2sv_enforced.security_2sv_enforced.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_enforced.security_2sv_enforced import ( + security_2sv_enforced, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_2sv_enforced() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins_test.py b/tests/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins_test.py new file mode 100644 index 0000000000..522164f341 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_2sv_hardware_keys_admins/security_2sv_hardware_keys_admins_test.py @@ -0,0 +1,126 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurity2svHardwareKeysAdmins: + def test_pass_passkey_only(self): + """Test PASS when 2SV enforcement requires security keys 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.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins import ( + security_2sv_hardware_keys_admins, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + two_sv_allowed_factor_set="PASSKEY_ONLY" + ) + + check = security_2sv_hardware_keys_admins() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "security keys only" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_all_methods_allowed(self): + """Test FAIL when 2SV enforcement allows ALL methods""" + 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.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins import ( + security_2sv_hardware_keys_admins, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(two_sv_allowed_factor_set="ALL") + + check = security_2sv_hardware_keys_admins() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ALL" in findings[0].status_extended + + def test_fail_none_not_configured(self): + """Test FAIL when 2SV enforcement factor is not configured (None)""" + 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.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins import ( + security_2sv_hardware_keys_admins, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(two_sv_allowed_factor_set=None) + + check = security_2sv_hardware_keys_admins() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not 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.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins import ( + security_2sv_hardware_keys_admins, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_2sv_hardware_keys_admins() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured_test.py b/tests/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured_test.py new file mode 100644 index 0000000000..86f25229ae --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_advanced_protection_configured/security_advanced_protection_configured_test.py @@ -0,0 +1,163 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityAdvancedProtectionConfigured: + def test_pass_properly_configured(self): + """Test PASS when Advanced Protection is configured with enrollment and codes blocked""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_advanced_protection_configured.security_advanced_protection_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_advanced_protection_configured.security_advanced_protection_configured import ( + security_advanced_protection_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + advanced_protection_enrollment=True, + advanced_protection_security_code_option="CODES_NOT_ALLOWED", + ) + + check = security_advanced_protection_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "configured" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_codes_allowed(self): + """Test FAIL when enrollment is enabled but security codes are allowed""" + 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.security.security_advanced_protection_configured.security_advanced_protection_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_advanced_protection_configured.security_advanced_protection_configured import ( + security_advanced_protection_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + advanced_protection_enrollment=True, + advanced_protection_security_code_option="ALLOWED_WITHOUT_REMOTE_ACCESS", + ) + + check = security_advanced_protection_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not properly configured" in findings[0].status_extended + + def test_fail_enrollment_disabled(self): + """Test FAIL when enrollment is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_advanced_protection_configured.security_advanced_protection_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_advanced_protection_configured.security_advanced_protection_configured import ( + security_advanced_protection_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + advanced_protection_enrollment=False, + ) + + check = security_advanced_protection_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not properly configured" in findings[0].status_extended + + def test_fail_enrollment_unset(self): + """Test FAIL when enrollment is None (not configured)""" + 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.security.security_advanced_protection_configured.security_advanced_protection_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_advanced_protection_configured.security_advanced_protection_configured import ( + security_advanced_protection_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + advanced_protection_enrollment=None, + advanced_protection_security_code_option="CODES_NOT_ALLOWED", + ) + + check = security_advanced_protection_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enrollment is not 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.security.security_advanced_protection_configured.security_advanced_protection_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_advanced_protection_configured.security_advanced_protection_configured import ( + security_advanced_protection_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_advanced_protection_configured() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted_test.py b/tests/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted_test.py new file mode 100644 index 0000000000..ad11b50a02 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_app_access_restricted/security_app_access_restricted_test.py @@ -0,0 +1,124 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityAppAccessRestricted: + def test_pass_access_restricted(self): + """Test PASS when application access to Google services is restricted""" + 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.security.security_app_access_restricted.security_app_access_restricted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_app_access_restricted.security_app_access_restricted import ( + security_app_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(google_services_restricted=True) + + check = security_app_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "restricted" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_access_unrestricted(self): + """Test FAIL when application access to Google services is unrestricted""" + 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.security.security_app_access_restricted.security_app_access_restricted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_app_access_restricted.security_app_access_restricted import ( + security_app_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(google_services_restricted=False) + + check = security_app_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "unrestricted" in findings[0].status_extended + + def test_fail_none_not_configured(self): + """Test FAIL when application access is not configured (None)""" + 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.security.security_app_access_restricted.security_app_access_restricted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_app_access_restricted.security_app_access_restricted import ( + security_app_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(google_services_restricted=None) + + check = security_app_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not 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.security.security_app_access_restricted.security_app_access_restricted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_app_access_restricted.security_app_access_restricted import ( + security_app_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_app_access_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured_test.py b/tests/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured_test.py new file mode 100644 index 0000000000..402c7b4f2d --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_dlp_drive_rules_configured/security_dlp_drive_rules_configured_test.py @@ -0,0 +1,124 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityDlpDriveRulesConfigured: + def test_pass_dlp_rules_configured(self): + """Test PASS when DLP policies for Google Drive are configured""" + 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.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured import ( + security_dlp_drive_rules_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(dlp_drive_rules_exist=True) + + check = security_dlp_drive_rules_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "configured" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_dlp_rules(self): + """Test FAIL when no DLP policies for Google Drive are configured""" + 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.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured import ( + security_dlp_drive_rules_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(dlp_drive_rules_exist=False) + + check = security_dlp_drive_rules_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active" in findings[0].status_extended + + def test_fail_none_no_dlp_rules(self): + """Test FAIL when DLP rules existence is None (no active rules)""" + 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.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured import ( + security_dlp_drive_rules_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(dlp_drive_rules_exist=None) + + check = security_dlp_drive_rules_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active" 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.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_dlp_drive_rules_configured.security_dlp_drive_rules_configured import ( + security_dlp_drive_rules_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_dlp_drive_rules_configured() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted_test.py b/tests/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted_test.py new file mode 100644 index 0000000000..2e0aafc1fb --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_internal_apps_trusted/security_internal_apps_trusted_test.py @@ -0,0 +1,127 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityInternalAppsTrusted: + def test_pass_internal_apps_trusted(self): + """Test PASS when internal domain-owned apps are trusted""" + 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.security.security_internal_apps_trusted.security_internal_apps_trusted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_internal_apps_trusted.security_internal_apps_trusted import ( + security_internal_apps_trusted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(trust_internal_apps=True) + + check = security_internal_apps_trusted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "trusted" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_none_secure_default(self): + """Test PASS when internal apps trust is None (secure default)""" + 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.security.security_internal_apps_trusted.security_internal_apps_trusted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_internal_apps_trusted.security_internal_apps_trusted import ( + security_internal_apps_trusted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(trust_internal_apps=None) + + check = security_internal_apps_trusted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_internal_apps_not_trusted(self): + """Test FAIL when internal domain-owned apps are not trusted""" + 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.security.security_internal_apps_trusted.security_internal_apps_trusted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_internal_apps_trusted.security_internal_apps_trusted import ( + security_internal_apps_trusted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(trust_internal_apps=False) + + check = security_internal_apps_trusted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not trusted" 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.security.security_internal_apps_trusted.security_internal_apps_trusted.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_internal_apps_trusted.security_internal_apps_trusted import ( + security_internal_apps_trusted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_internal_apps_trusted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled_test.py b/tests/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled_test.py new file mode 100644 index 0000000000..6dc9d53416 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_less_secure_apps_disabled/security_less_secure_apps_disabled_test.py @@ -0,0 +1,127 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityLessSecureAppsDisabled: + def test_pass_less_secure_apps_disabled(self): + """Test PASS when less secure app access is explicitly 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.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled import ( + security_less_secure_apps_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(less_secure_apps_allowed=False) + + check = security_less_secure_apps_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_none_secure_default(self): + """Test PASS when less secure app access is None (secure default)""" + 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.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled import ( + security_less_secure_apps_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(less_secure_apps_allowed=None) + + check = security_less_secure_apps_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_less_secure_apps_enabled(self): + """Test FAIL when less secure app access is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled import ( + security_less_secure_apps_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(less_secure_apps_allowed=True) + + check = security_less_secure_apps_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" 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.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_less_secure_apps_disabled.security_less_secure_apps_disabled import ( + security_less_secure_apps_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_less_secure_apps_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured_test.py b/tests/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured_test.py new file mode 100644 index 0000000000..8cc084ca84 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_login_challenges_configured/security_login_challenges_configured_test.py @@ -0,0 +1,127 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityLoginChallengesConfigured: + def test_pass_employee_id_challenge_disabled(self): + """Test PASS when employee ID login challenge is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_login_challenges_configured.security_login_challenges_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_login_challenges_configured.security_login_challenges_configured import ( + security_login_challenges_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(login_challenge_employee_id=False) + + check = security_login_challenges_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_none_secure_default(self): + """Test PASS when employee ID login challenge is None (secure default)""" + 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.security.security_login_challenges_configured.security_login_challenges_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_login_challenges_configured.security_login_challenges_configured import ( + security_login_challenges_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(login_challenge_employee_id=None) + + check = security_login_challenges_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_employee_id_challenge_enabled(self): + """Test FAIL when employee ID login challenge is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_login_challenges_configured.security_login_challenges_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_login_challenges_configured.security_login_challenges_configured import ( + security_login_challenges_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(login_challenge_employee_id=True) + + check = security_login_challenges_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" 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.security.security_login_challenges_configured.security_login_challenges_configured.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_login_challenges_configured.security_login_challenges_configured import ( + security_login_challenges_configured, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_login_challenges_configured() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong_test.py b/tests/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong_test.py new file mode 100644 index 0000000000..850bf243a9 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_password_policy_strong/security_password_policy_strong_test.py @@ -0,0 +1,210 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityPasswordPolicyStrong: + def test_pass_strong_password_policy(self): + """Test PASS when password policy meets CIS requirements""" + 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.security.security_password_policy_strong.security_password_policy_strong.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_password_policy_strong.security_password_policy_strong import ( + security_password_policy_strong, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + password_minimum_length=14, + password_allowed_strength="STRONG", + password_allow_reuse=False, + password_enforce_at_login=True, + password_expiration_duration="31536000s", + ) + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "meets CIS requirements" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_weak_password_policy(self): + """Test FAIL when password policy does not meet CIS requirements""" + 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.security.security_password_policy_strong.security_password_policy_strong.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_password_policy_strong.security_password_policy_strong import ( + security_password_policy_strong, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + password_minimum_length=8, + password_allowed_strength="STRONG", + password_allow_reuse=False, + password_enforce_at_login=False, + password_expiration_duration="0s", + ) + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "does not meet" in findings[0].status_extended + + def test_fail_none_all_defaults(self): + """Test FAIL when all password policy fields are None (defaults)""" + 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.security.security_password_policy_strong.security_password_policy_strong.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_password_policy_strong.security_password_policy_strong import ( + security_password_policy_strong, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + password_minimum_length=None, + password_allowed_strength=None, + password_allow_reuse=None, + password_enforce_at_login=None, + password_expiration_duration=None, + ) + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "does not meet" in findings[0].status_extended + + def test_fail_strength_unset_treated_as_missing(self): + """Test FAIL when password_allowed_strength is None even with other fields strong""" + 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.security.security_password_policy_strong.security_password_policy_strong.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_password_policy_strong.security_password_policy_strong import ( + security_password_policy_strong, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + password_minimum_length=14, + password_allowed_strength=None, + password_allow_reuse=False, + password_enforce_at_login=True, + password_expiration_duration="31536000s", + ) + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "password strength is not configured" in findings[0].status_extended + + def test_fail_min_length_unset_reports_not_configured(self): + """Test FAIL message uses 'not configured' when password_minimum_length is None""" + 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.security.security_password_policy_strong.security_password_policy_strong.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_password_policy_strong.security_password_policy_strong import ( + security_password_policy_strong, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies( + password_minimum_length=None, + password_allowed_strength="STRONG", + password_allow_reuse=False, + password_enforce_at_login=True, + password_expiration_duration="31536000s", + ) + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "minimum length is not 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.security.security_password_policy_strong.security_password_policy_strong.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_password_policy_strong.security_password_policy_strong import ( + security_password_policy_strong, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited_test.py b/tests/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited_test.py new file mode 100644 index 0000000000..668b6aea78 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_session_duration_limited/security_session_duration_limited_test.py @@ -0,0 +1,152 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecuritySessionDurationLimited: + def test_pass_session_12_hours(self): + """Test PASS when session duration is set to 12 hours""" + 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.security.security_session_duration_limited.security_session_duration_limited.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_session_duration_limited.security_session_duration_limited import ( + security_session_duration_limited, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(web_session_duration="43200s") + + check = security_session_duration_limited() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "12 hours" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_long_session_duration(self): + """Test FAIL when session duration is too long (336 hours)""" + 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.security.security_session_duration_limited.security_session_duration_limited.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_session_duration_limited.security_session_duration_limited import ( + security_session_duration_limited, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(web_session_duration="1209600s") + + check = security_session_duration_limited() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "336 hours" in findings[0].status_extended + + def test_fail_none_not_configured(self): + """Test FAIL when session duration is not explicitly configured (None)""" + 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.security.security_session_duration_limited.security_session_duration_limited.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_session_duration_limited.security_session_duration_limited import ( + security_session_duration_limited, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(web_session_duration=None) + + check = security_session_duration_limited() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_fail_unparseable_duration(self): + """Test FAIL when session duration has an unexpected format instead of crashing""" + 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.security.security_session_duration_limited.security_session_duration_limited.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_session_duration_limited.security_session_duration_limited import ( + security_session_duration_limited, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(web_session_duration="invalid") + + check = security_session_duration_limited() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not parseable" 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.security.security_session_duration_limited.security_session_duration_limited.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_session_duration_limited.security_session_duration_limited import ( + security_session_duration_limited, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_session_duration_limited() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled_test.py b/tests/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled_test.py new file mode 100644 index 0000000000..d9a1f306b8 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_super_admin_recovery_disabled/security_super_admin_recovery_disabled_test.py @@ -0,0 +1,127 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecuritySuperAdminRecoveryDisabled: + def test_pass_recovery_disabled(self): + """Test PASS when Super Admin account recovery is explicitly 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.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled import ( + security_super_admin_recovery_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(super_admin_recovery_enabled=False) + + check = security_super_admin_recovery_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_none_secure_default(self): + """Test PASS when Super Admin account recovery is None (secure default)""" + 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.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled import ( + security_super_admin_recovery_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(super_admin_recovery_enabled=None) + + check = security_super_admin_recovery_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_recovery_enabled(self): + """Test FAIL when Super Admin account recovery is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled import ( + security_super_admin_recovery_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(super_admin_recovery_enabled=True) + + check = security_super_admin_recovery_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" 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.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_super_admin_recovery_disabled.security_super_admin_recovery_disabled import ( + security_super_admin_recovery_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_super_admin_recovery_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled_test.py b/tests/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled_test.py new file mode 100644 index 0000000000..c8a519dbe0 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/security_user_recovery_enabled/security_user_recovery_enabled_test.py @@ -0,0 +1,124 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.security.security_service import ( + SecurityPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSecurityUserRecoveryEnabled: + def test_pass_recovery_enabled(self): + """Test PASS when user account recovery is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_user_recovery_enabled.security_user_recovery_enabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_user_recovery_enabled.security_user_recovery_enabled import ( + security_user_recovery_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(user_recovery_enabled=True) + + check = security_user_recovery_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "enabled" in findings[0].status_extended + assert findings[0].resource_name == "Security Policies" + assert findings[0].resource_id == "securityPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_recovery_disabled(self): + """Test FAIL when user account recovery is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.security.security_user_recovery_enabled.security_user_recovery_enabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_user_recovery_enabled.security_user_recovery_enabled import ( + security_user_recovery_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(user_recovery_enabled=False) + + check = security_user_recovery_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_fail_none_not_configured(self): + """Test FAIL when user account recovery is None (not explicitly configured)""" + 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.security.security_user_recovery_enabled.security_user_recovery_enabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_user_recovery_enabled.security_user_recovery_enabled import ( + security_user_recovery_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SecurityPolicies(user_recovery_enabled=None) + + check = security_user_recovery_enabled() + 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.security.security_user_recovery_enabled.security_user_recovery_enabled.security_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.security.security_user_recovery_enabled.security_user_recovery_enabled import ( + security_user_recovery_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SecurityPolicies() + + check = security_user_recovery_enabled() + findings = check.execute() + + assert len(findings) == 0