From fb7064401b3d683caf6a063e553cc5d73506547e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Tue, 1 Sep 2026 09:35:39 +0200 Subject: [PATCH] feat(compliance): add CIS 1.4 google workspace compliance (#12513) Co-authored-by: Lydia Vilchez --- README.md | 2 +- .../cis-1.4-googleworkspace.added.md | 1 + ...gleworkspace-2sv-policy-overrides.fixed.md | 1 + ...leworkspace-alert-center-delivery.fixed.md | 1 + ...space-cis-full-audit-procedures.changed.md | 1 + ...leworkspace-compliance-mappings.changed.md | 1 + ...rkspace-password-strength-default.fixed.md | 1 + .../cis_1.3_googleworkspace.json | 4 +- .../cis_1.4_googleworkspace.json | 1943 +++++++++++++++++ .../cisa_scuba_0.6_googleworkspace.json | 1 - .../googleworkspace/lib/service/service.py | 36 +- ..._spoofing_protection_enabled.metadata.json | 2 +- ...mail_domain_spoofing_protection_enabled.py | 42 +- ..._spoofing_protection_enabled.metadata.json | 2 +- ...ployee_name_spoofing_protection_enabled.py | 40 +- ..._spoofing_protection_enabled.metadata.json | 2 +- ...mail_groups_spoofing_protection_enabled.py | 55 +- ..._spoofing_protection_enabled.metadata.json | 2 +- ...ound_domain_spoofing_protection_enabled.py | 34 +- .../services/gmail/lib/__init__.py | 0 .../services/gmail/lib/spoofing.py | 33 + .../services/rules/lib/__init__.py | 0 .../services/rules/lib/alerts.py | 127 ++ ...ege_granted_alert_configured.metadata.json | 2 +- ...dmin_privilege_granted_alert_configured.py | 60 +- ...ee_spoofing_alert_configured.metadata.json | 2 +- ...mail_employee_spoofing_alert_configured.py | 60 +- ...ked_attacks_alert_configured.metadata.json | 2 +- ...ernment_backed_attacks_alert_configured.py | 60 +- ...ed_password_alert_configured.metadata.json | 2 +- .../rules_leaked_password_alert_configured.py | 60 +- ...ord_changed_alert_configured.metadata.json | 2 +- ...rules_password_changed_alert_configured.py | 60 +- .../services/rules/rules_service.py | 14 +- ..._suspension_alert_configured.metadata.json | 2 +- ...us_activity_suspension_alert_configured.py | 60 +- ...cious_login_alert_configured.metadata.json | 2 +- ...rules_suspicious_login_alert_configured.py | 60 +- ...matic_login_alert_configured.metadata.json | 2 +- ...ous_programmatic_login_alert_configured.py | 60 +- .../services/security/lib/__init__.py | 0 .../services/security/lib/durations.py | 89 + .../services/security/lib/scope.py | 46 + .../security_2sv_enforced.metadata.json | 4 +- .../security_2sv_enforced.py | 183 +- ...ity_2sv_hardware_keys_admins.metadata.json | 2 +- .../security_2sv_hardware_keys_admins.py | 176 +- ..._login_challenges_configured.metadata.json | 2 +- .../security_login_challenges_configured.py | 10 +- .../security_password_policy_strong.py | 41 +- .../services/security/security_service.py | 40 +- .../googleworkspace_fixtures.py | 15 +- .../service/googleworkspace_service_test.py | 47 +- ...domain_spoofing_protection_enabled_test.py | 68 +- ...e_name_spoofing_protection_enabled_test.py | 36 +- ...groups_spoofing_protection_enabled_test.py | 30 + ...domain_spoofing_protection_enabled_test.py | 36 +- .../services/gmail/lib/spoofing_test.py | 34 + .../services/rules/lib/alerts_test.py | 154 ++ ...privilege_granted_alert_configured_test.py | 74 +- ...employee_spoofing_alert_configured_test.py | 74 +- ...nt_backed_attacks_alert_configured_test.py | 80 +- ...s_leaked_password_alert_configured_test.py | 111 +- ..._password_changed_alert_configured_test.py | 74 +- .../services/rules/rules_service_test.py | 124 +- ...tivity_suspension_alert_configured_test.py | 80 +- ..._suspicious_login_alert_configured_test.py | 78 +- ...rogrammatic_login_alert_configured_test.py | 80 +- .../googleworkspace_security_service_test.py | 138 ++ .../services/security/lib/durations_test.py | 97 + .../security_2sv_enforced_test.py | 385 ++-- .../security_2sv_hardware_keys_admins_test.py | 352 ++- .../security_password_policy_strong_test.py | 208 +- 73 files changed, 4941 insertions(+), 868 deletions(-) create mode 100644 prowler/changelog.d/cis-1.4-googleworkspace.added.md create mode 100644 prowler/changelog.d/googleworkspace-2sv-policy-overrides.fixed.md create mode 100644 prowler/changelog.d/googleworkspace-alert-center-delivery.fixed.md create mode 100644 prowler/changelog.d/googleworkspace-cis-full-audit-procedures.changed.md create mode 100644 prowler/changelog.d/googleworkspace-compliance-mappings.changed.md create mode 100644 prowler/changelog.d/googleworkspace-password-strength-default.fixed.md create mode 100644 prowler/compliance/googleworkspace/cis_1.4_googleworkspace.json create mode 100644 prowler/providers/googleworkspace/services/gmail/lib/__init__.py create mode 100644 prowler/providers/googleworkspace/services/gmail/lib/spoofing.py create mode 100644 prowler/providers/googleworkspace/services/rules/lib/__init__.py create mode 100644 prowler/providers/googleworkspace/services/rules/lib/alerts.py create mode 100644 prowler/providers/googleworkspace/services/security/lib/__init__.py create mode 100644 prowler/providers/googleworkspace/services/security/lib/durations.py create mode 100644 prowler/providers/googleworkspace/services/security/lib/scope.py create mode 100644 tests/providers/googleworkspace/services/gmail/lib/spoofing_test.py create mode 100644 tests/providers/googleworkspace/services/rules/lib/alerts_test.py create mode 100644 tests/providers/googleworkspace/services/security/lib/durations_test.py diff --git a/README.md b/README.md index de5c235dd7..ad69260cb0 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically | MongoDB Atlas | 10 | 3 | 1 | 8 | Official | UI, API, CLI | | LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI | | Image | N/A | N/A | N/A | N/A | Official | UI, API, CLI | -| Google Workspace | 65 | 11 | 3 | 6 | Official | UI, API, CLI | +| Google Workspace | 65 | 11 | 4 | 6 | Official | UI, API, CLI | | OpenStack | 34 | 5 | 1 | 9 | Official | UI, API, CLI | | Vercel | 26 | 6 | 1 | 8 | Official | UI, API, CLI | | Okta | 29 | 8 | 2 | 2 | Official | UI, API, CLI | diff --git a/prowler/changelog.d/cis-1.4-googleworkspace.added.md b/prowler/changelog.d/cis-1.4-googleworkspace.added.md new file mode 100644 index 0000000000..4b3fbc58c7 --- /dev/null +++ b/prowler/changelog.d/cis-1.4-googleworkspace.added.md @@ -0,0 +1 @@ +CIS Google Workspace Foundations Benchmark v1.4.0 compliance framework diff --git a/prowler/changelog.d/googleworkspace-2sv-policy-overrides.fixed.md b/prowler/changelog.d/googleworkspace-2sv-policy-overrides.fixed.md new file mode 100644 index 0000000000..759f6c9b52 --- /dev/null +++ b/prowler/changelog.d/googleworkspace-2sv-policy-overrides.fixed.md @@ -0,0 +1 @@ +`security_2sv_enforced` and `security_2sv_hardware_keys_admins` report MANUAL instead of judging domain-wide values that a group or a sub-organizational unit overrides, or that were dropped because the root organizational unit could not be resolved; a domain-wide failure is still reported as such, with the override noted diff --git a/prowler/changelog.d/googleworkspace-alert-center-delivery.fixed.md b/prowler/changelog.d/googleworkspace-alert-center-delivery.fixed.md new file mode 100644 index 0000000000..57e591c461 --- /dev/null +++ b/prowler/changelog.d/googleworkspace-alert-center-delivery.fixed.md @@ -0,0 +1 @@ +`rules_*_alert_configured` checks no longer pass a rule whose delivery to the alert center is disabled, the setting behind the benchmark's "Ensure that Alerts is set to On" diff --git a/prowler/changelog.d/googleworkspace-cis-full-audit-procedures.changed.md b/prowler/changelog.d/googleworkspace-cis-full-audit-procedures.changed.md new file mode 100644 index 0000000000..c99ad0f0e4 --- /dev/null +++ b/prowler/changelog.d/googleworkspace-cis-full-audit-procedures.changed.md @@ -0,0 +1 @@ +Google Workspace checks mapped to CIS evaluate the full audit procedure instead of a single condition, so Gmail spoofing actions, 2-Step Verification, password expiration and alert severity left on Google's defaults no longer pass diff --git a/prowler/changelog.d/googleworkspace-compliance-mappings.changed.md b/prowler/changelog.d/googleworkspace-compliance-mappings.changed.md new file mode 100644 index 0000000000..b4fb6924d2 --- /dev/null +++ b/prowler/changelog.d/googleworkspace-compliance-mappings.changed.md @@ -0,0 +1 @@ +`security_login_challenges_configured` and `security_2sv_enforced` unmapped from CIS Google Workspace 4.1.4.1 and CISA SCuBA 0.6 `GWS.COMMONCONTROLS.1.1`, whose Post-SSO verification and phishing-resistant MFA requirements neither check can prove diff --git a/prowler/changelog.d/googleworkspace-password-strength-default.fixed.md b/prowler/changelog.d/googleworkspace-password-strength-default.fixed.md new file mode 100644 index 0000000000..0c80b92d24 --- /dev/null +++ b/prowler/changelog.d/googleworkspace-password-strength-default.fixed.md @@ -0,0 +1 @@ +`security_password_policy_strong` no longer fails a domain that never touched the password strength setting: Google enforces strong passwords by default, so an unset value is the secure default and not a missing configuration diff --git a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json index 167842af4b..8da36da5e2 100644 --- a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json @@ -1498,9 +1498,7 @@ { "Id": "4.1.4.1", "Description": "Ensure login challenges are enforced", - "Checks": [ - "security_login_challenges_configured" - ], + "Checks": [], "Attributes": [ { "Section": "4 Security", diff --git a/prowler/compliance/googleworkspace/cis_1.4_googleworkspace.json b/prowler/compliance/googleworkspace/cis_1.4_googleworkspace.json new file mode 100644 index 0000000000..7958a7772c --- /dev/null +++ b/prowler/compliance/googleworkspace/cis_1.4_googleworkspace.json @@ -0,0 +1,1943 @@ +{ + "Framework": "CIS", + "Name": "CIS Google Workspace Foundations Benchmark v1.4.0", + "Version": "1.4", + "Provider": "GoogleWorkspace", + "Description": "The CIS Google Workspace Foundations Benchmark provides prescriptive guidance for establishing a secure configuration posture for Google Workspace. This benchmark covers Directory, Devices, Apps, Security, Reporting, and Rules configurations.", + "Requirements": [ + { + "Id": "1.1.1", + "Description": "Ensure that between two and four global admins are designated", + "Checks": [ + "directory_super_admin_count" + ], + "Attributes": [ + { + "Section": "1 Directory", + "SubSection": "1.1 Users", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Having more than one Super Admin account is needed primarily so that a single point of failure can be avoided. Also, for larger organizations, having multiple Super Admins can be useful for workload balancing purposes.", + "RationaleStatement": "From a security point of view, having only a single Super Admin Account can be problematic if this user were unavailable for an extended period of time. Also, Super Admin accounts should never be shared amongst multiple users. From a security point of view, having a large number of Super Admin accounts is a bad practice. In general, all users should be assigned the least privileges needed to do their job. This includes Administrators since not everyone that needs to \"Administer Something\" needs to be a Super Admin. Google Workspaces provides many predefined Administration Roles and also allows the creation of Custom Roles with very granular permission selection.", + "ImpactStatement": "There should be no user impact, but Administrators should have a normal (low privilege) and an Administrative (high privilege) account.", + "RemediationProcedure": "Create at least one additional account with a Super Admin role if there is only 1 Super Admin account. If more that 4 accounts with Super Admin access, reduce the number of accounts with a Super Admin role. NOTE: A new account should be created vs adding this role to an existing account since Administration tasks should be done through separate Admin accounts.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Go to Directory and click on Users, this will show a list of all users 3. Click on + Add a filter, select Admin role, check the Super admin box, and then select Apply 4. The list of Users displayed will only be those with the Super Admin role 5. Make sure more than one (1) user is listed 6. Make sure no more than four (4) users are listed", + "AdditionalInformation": "", + "DefaultValue": "All Google Workspace tenants will have one Super Admin initially.", + "References": "" + } + ] + }, + { + "Id": "1.1.2", + "Description": "Ensure super admin accounts are used only for super admin activities", + "Checks": [ + "directory_super_admin_only_admin_roles" + ], + "Attributes": [ + { + "Section": "1 Directory", + "SubSection": "1.1 Users", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Super admin accounts have access to all features in the Google Admin console and Admin API and can manage every aspect of your organization's account. Super admins also have full access to all users' calendars and event details. It is recommended to give each super administrator two accounts. One for their super admin account and a second account for daily activities. Users should only sign in to a super admin account to perform super admin tasks, such as setting up 2-Step Verification (2SV), managing billing and user licenses, or helping another admin recover their account. Super administrators should use a separate, non-admin account for day-to-day activities. Super admins should sign in as needed to do specific tasks and then sign out. Leaving super admin accounts sign-in can increase exposure to phishing attacks.", + "RationaleStatement": "Use the super admin account only when needed. Delegate administrator tasks to user accounts with limited admin roles. Use the least privilege approach, where each user has access to the resources and tools needed for their typical tasks. For example, you could grant an admin permissions to create user accounts and reset passwords, but not let them delete user accounts.", + "ImpactStatement": "Super admin users will have to switch accounts as well as utilize login/logout functionality when performing administrative tasks.", + "RemediationProcedure": "For every Super admin that is also a Delegated admin account, either create a Delegated admin account for the user of elevate or their existing non-admin account to a Delegated admin account.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Go to Directory and click on Users, this will show a list of all users 3. Click on + Add a filter, select Admin role, check the Super admin box, and then select Apply 4. The list of Users displayed will only be those with the Super Admin role 5. Click on + Add a filter, select Admin role, check the Delegated admin box, and then select Apply 6. Verify that there are no users in both the Super admin and Delegated admin roles", + "AdditionalInformation": "", + "DefaultValue": "N/A", + "References": "https://support.google.com/a/answer/179832?hl=en" + } + ] + }, + { + "Id": "1.2.1.1", + "Description": "Ensure directory data access is externally restricted", + "Checks": [], + "Attributes": [ + { + "Section": "1 Directory", + "SubSection": "1.2 Directory Settings", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configure Google Workspace's external directory sharing to prevent unrestricted directory data access.", + "RationaleStatement": "If your organization uses third-party apps that integrate with your Google services, you control how much Directory information the external apps can access. If you allow directory access, your users have a better experience with external apps. For example, when they use a third-party mail app, they want to find domain contacts and have email addresses automatically complete. The app needs access to Directory data to make this happen. However, this has the ability to share ALL domain AND public data with the connected third-party app. Public data and authenticated user basic profile fields — Share publicly visible domain profile data with external apps and APIs. Also share the authenticated user's name, photo, and email address to enable Google Sign-In if the appropriate scopes are granted. Other non-public profile fields for the authenticated user aren't shared. All the non-public profile information of other users in the domain aren't shared. Domain and public data — (Default) Share all Directory information that’s shared with your domain and public data. This information includes profile information for users in your domain, shared external contacts, and Google+ profile names and photos.", + "ImpactStatement": "The External directory sharing setting applies only to the following APIs and the Apps Scripts or third-party Marketplace apps that use those APIs: Google People API Google CardDAV API Google Contacts API v3 The setting applies only to third-party apps, such as iOS Mail and iOS Contacts (when enrolled on an iOS device via Add Account and then Google), third-party Contacts apps (on Android). The setting doesn't apply to Google products, including mobile apps, such as the following Gmail, Contacts (on Android), Inbox, Meet, and other Google mobile apps iOS Mail and iOS Contacts using Google Sync (when enrolled on an iOS device through Add Account and then Exchange) Workspace Sync for Microsoft Outlook", + "RemediationProcedure": "To configure this setting via the Google Workspace Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Open the collapsed menu via \"hamburger button \\ 3 horizontal lines\" 3. Under Directory, select Directory settings 4. Under Sharing settings, select External Directory sharing 5. Select Authenticated user basic profile fields", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Open the collapsed menu via \"hamburger button \\ 3 horizontal lines\" 3. Under Directory, select Directory settings 4. Under Sharing settings, select External Directory sharing 5. Ensure Authenticated user basic profile fields is set 6. Select Save", + "AdditionalInformation": "", + "DefaultValue": "• External Directory sharing = Domain and public data", + "References": "" + } + ] + }, + { + "Id": "3.1.1.1.1", + "Description": "Ensure external sharing options for primary calendars are configured", + "Checks": [ + "calendar_external_sharing_primary_calendar" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.1 Calendar", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control how much calendar information users in your organization can share externally.", + "RationaleStatement": "Prevent data leakage by restricting the amount of information that is externally viewable when a user shares their calendar with someone external to your organization.", + "ImpactStatement": "Once you limit external sharing for your organization, users can't exceed these limits when sharing individual events. For example, if you limit your organization's external sharing to Free/Busy, events with Public visibility are only shared as Free/Busy. External mobile users who previously synced events may keep seeing restricted details. That access stops when their device is wiped and re-synced. If you lower the external sharing level, people outside your organization may lose access to calendars they could previously see.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Sharing settings, select External sharing options for primary calendars 6. Select Only free/busy information (hide event details) 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Sharing settings, select External sharing options for primary calendars 6. Ensure Only free/busy information (hide event details) is selected", + "AdditionalInformation": "", + "DefaultValue": "External sharing options for primary calendars is Only free/busy information (hide event details)", + "References": "" + } + ] + }, + { + "Id": "3.1.1.1.2", + "Description": "Ensure internal sharing options for primary calendars are configured", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.1 Calendar", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Control how much calendar information users in your organization can share internally.", + "RationaleStatement": "In general, not everyone in the organization needs to know the schedule details of everyone else (operational security). Free/busy indication is enough for most people.", + "ImpactStatement": "This will be the default for the user's primary calendar. The user can override this setting to allow other specific users greater visibility of their calendar.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Sharing settings, select Internal sharing options for primary calendars 6. Select Only free/busy information (hide event details) 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Sharing settings, select Internal sharing options for primary calendars 6. Ensure Only free/busy information (hide event details) is selected", + "AdditionalInformation": "", + "DefaultValue": "Internal sharing options for primary calendars is Share all information", + "References": "" + } + ] + }, + { + "Id": "3.1.1.1.3", + "Description": "Ensure external invitation warnings for Google Calendar are configured", + "Checks": [ + "calendar_external_invitations_warning" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.1 Calendar", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configure Google Calendar to warn users when inviting guest outside your domain.", + "RationaleStatement": "When your users create a Google Calendar event that includes one or more guests from outside of your domain, they are prompted to confirm whether it’s OK to include external guests in the event invitation, assisting in the prevention of unintentional data leakage.", + "ImpactStatement": "Users will be prompted to allow the inclusion of external guests in an event invitation.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Sharing settings, select External Invitations 6. Set Warn users when inviting guests outside of the domain to checked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Sharing settings, select External invitations 6. Ensure Warn users when inviting guests outside of the domain is checked", + "AdditionalInformation": "", + "DefaultValue": "Warn users when inviting guests outside of the domain is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.1.2.1", + "Description": "Ensure external sharing options for secondary calendars are configured", + "Checks": [ + "calendar_external_sharing_secondary_calendar" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.1 Calendar", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control how much calendar information users in your organization can share externally.", + "RationaleStatement": "Prevent data leakage by restricting the amount of information is externally viewable when a user shares their calendar with someone external to your organization.", + "ImpactStatement": "Once you limit external sharing for your organization, users can't exceed these limits when sharing individual events. For example, if you limit your organization's external sharing to Free/Busy, events with Public visibility are only shared as Free/Busy. External mobile users who previously synced events may keep seeing restricted details. That access stops when their device is wiped and re-synced. If you lower the external sharing level, people outside your organization may lose access to calendars they could previously see.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under General settings, select External sharing options for secondary calendars 6. Select Only free/busy information (hide event details) 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under General settings, select External sharing options for secondary calendars 6. Ensure Only free/busy information (hide event details) is selected", + "AdditionalInformation": "", + "DefaultValue": "External sharing options for secondary calendars is Share all information, but outsiders cannot change calendars", + "References": "" + } + ] + }, + { + "Id": "3.1.1.2.2", + "Description": "Ensure internal sharing options for secondary calendars are configured", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.1 Calendar", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Control how much calendar information users in your organization can share internally.", + "RationaleStatement": "In general, not everyone in the organization needs to know the schedule details of everyone else (operational security). Free/busy indication is enough for most people.", + "ImpactStatement": "This will be the default for the user's secondary calendars. The user can override this setting to allow other specific users greater visibility of their calendars.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under General settings, select Internal sharing options for secondary calendars 6. Select Only free/busy information (hide event details) 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under General settings, select Internal sharing options for secondary calendars 6. Ensure Only free/busy information (hide event details) is selected", + "AdditionalInformation": "", + "DefaultValue": "Internal sharing options for secondary calendars is Share all information", + "References": "" + } + ] + }, + { + "Id": "3.1.1.3.1", + "Description": "Ensure calendar web offline is disabled", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.1 Calendar", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Limit who is allowed offline calendar access.", + "RationaleStatement": "When enabled, users can turn on offline use for each computer they use. Data is stored on the computer until offline use is turned off by the user. In this case, the organization can lose control of where its data is stored (for this user). Care should be taken regarding which users and groups have this capability enabled.", + "ImpactStatement": "Users will not be able to access their calendars offline.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Advanced settings, select Calendar web offline 6. Set Allow using Calendar on the web when offline to unchecked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Calendar 5. Under Advanced settings, select Calendar web offline 6. Ensure Allow using Calendar on the web when offline is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Allow using Calendar on the web when offline is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.1.1", + "Description": "Ensure users are warned when they share a file outside their domain", + "Checks": [ + "drive_external_sharing_warn_users" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Warn the user when they try and share a file and/or shared drive externally.", + "RationaleStatement": "The user may not realize the potential account is external to the organization. Providing a warning allows the user an opportunity to know this and possibly reassess this sharing.", + "ImpactStatement": "None, except an additional warning. Sharing can still occur.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Drive and Docs 4. Select Sharing Settings 5. Select Sharing Options 6. Under Sharing outside of 7. Set ON - Files owned by users in can be shared outside of . This applies to files in all shared drives as well. to checked. Also, set the sub-setting For files owned by users in warn when sharing outside of to checked. 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Drive and Docs 4. Select Sharing Settings 5. Select Sharing Options 6. Under Sharing outside of 7. Ensure ON - Files owned by users in can be shared outside of . This applies to files in all shared drives as well. is checked. Also, ensure the sub-setting For files owned by users in warn when sharing outside of is checked.", + "AdditionalInformation": "", + "DefaultValue": "For files owned by users in warn when sharing outside of is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.1.2", + "Description": "Ensure users cannot publish files to the web or make visible to the world as public or unlisted", + "Checks": [ + "drive_publishing_files_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "You should control the publishing of documents to the web or making them visable to the world as public or unlisted.", + "RationaleStatement": "Attackers will often attempt to expose sensitive information to external entities through sharing, and restricting the methods that your users can share documents with will reduce that surface area. This setting is only applicable if ON - Files owned by users in can be shared outside of . This applies to files in all shared drives as well is selected, but should be configured as described below to prevent unintentional document publishing.", + "ImpactStatement": "Enabling this feature will prevent users from publishing documents on the web or making them visible to the world as public or unlisted files.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Sharing options 6. Under Sharing outside of - ON - Files owned by users in can be shared outside of . This applies to files in all shared drives as well, set When sharing outside of is allowed, users in can make files and published web content visible to anyone with the link to unchecked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Sharing options 6. Under Sharing outside of - ON - Files owned by users in can be shared outside of . This applies to files in all shared drives as well, ensure When sharing outside of is allowed, users in can make files and published web content visible to anyone with the link is unchecked", + "AdditionalInformation": "", + "DefaultValue": "When sharing outside of is allowed, users in can make files and published web content visible to anyone with the link is Checked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.1.3", + "Description": "Ensure document sharing is being controlled by domain with allowlists", + "Checks": [ + "drive_sharing_allowlisted_domains" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "You should control sharing of documents to external domains by either blocking domains or only allowing sharing with specific named domains.", + "RationaleStatement": "Attackers will often attempt to expose sensitive information to external entities through sharing, and restricting the domains that your users can share documents with will reduce that surface area.", + "ImpactStatement": "Enabling this feature will prevent users from sharing documents with domains outside of the organization unless allowed.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Sharing options 6. Under Sharing outside of , select ALLOWLISTED DOMAINS - Files owned by users in can be shared with Google Accounts in compatible allowlisted domains. 7. Set Warn when files owned by users or shared drives in are shared with users in allowlisted domains to checked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Sharing options 6. Under Sharing outside of , ensure ALLOWLISTED DOMAINS - Files owned by users in can be shared with Google Accounts in compatible allowlisted domains. is selected 7. Ensure Warn when files owned by users or shared drives in are shared with users in allowlisted domains is checked", + "AdditionalInformation": "", + "DefaultValue": "Sharing outside of is ON - Files owned by users in can be shared outside of . This applies to files in all shared drives as well.", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.1.4", + "Description": "Ensure users are warned when they share a file with users in an allowlisted domain", + "Checks": [ + "drive_warn_sharing_with_allowlisted_domains" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Warn the user when they try and share a file and/or shared drive with users in an allowlisted domain.", + "RationaleStatement": "The user may not realize the potential account is external to the organization. Providing a warning allows the user an opportunity to know this and possibly reassess this sharing.", + "ImpactStatement": "None, except an additional warning. Sharing can still occur.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Drive and Docs 4. Select Sharing Settings 5. Select Sharing Options 6. Under Sharing outside of 7. Set ALLOWLISTED DOMAINS - Files owned by users or shared drives in BMDT-Group can be shared with Google accounts in compatible allowlisted domains. to checked. Also, set the sub-setting Warn when files owned by users or shared drives in are shared with users in allowlisted domains to checked. 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Drive and Docs 4. Select Sharing Settings 5. Select Sharing Options 6. Under Sharing outside of 7. Ensure ALLOWLISTED DOMAINS - Files owned by users or shared drives in BMDT-Group can be shared with Google accounts in compatible allowlisted domains is checked. Also, ensure the sub-setting Warn when files owned by users or shared drives in are shared with users in allowlisted domains is checked.", + "AdditionalInformation": "", + "DefaultValue": "For files owned by users in warn when sharing outside of is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.1.5", + "Description": "Ensure Access Checker is configured to limit file access", + "Checks": [ + "drive_access_checker_recipients_only" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "When a user shares a file via a Google product other than Docs or Drive (e.g. by pasting a link in Gmail), Google can check that the recipients have access. If not, when possible, Google will ask the user to pick how they want to share the file.", + "RationaleStatement": "In general, access should be restricted to the smallest group possible. In this case recipients only.", + "ImpactStatement": "Only recipients can access files. Recipients cannot share access with others by forwarding the email/link.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Drive and Docs 4. Select Sharing Settings 5. Select Sharing Options 6. Under Access Checker 7. Set Recipients only. to checked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Drive and Docs 4. Select Sharing Settings 5. Select Sharing Options 6. Under Access Checker 7. Ensure Recipients only. is checked", + "AdditionalInformation": "", + "DefaultValue": "Recipients only, suggested target audience, or public (no Google account required). is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.1.6", + "Description": "Ensure only users inside your organization can distribute content externally", + "Checks": [ + "drive_internal_users_distribute_content" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "You should control who is allowed to distribute organizational content to shared drives owned by another organization.", + "RationaleStatement": "Sharing and collaboration are key; however, only your users should have the authority over where company content is shared with to prevent unauthorized disclosures of information.", + "ImpactStatement": "Only people in your organization with Manager access to a shared drive can move files from that shared drive to a Drive location in a different organization. In addition, users in the selected organizational unit or group can copy content from their My Drive to a shared drive owned by a different organization.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Sharing options 6. Under Distributing content outside of , select - Only users in 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Sharing options 6. Under Distributing content outside of , ensure Only users in is selected", + "AdditionalInformation": "", + "DefaultValue": "Distributing content outside of is Anyone", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.2.1", + "Description": "Audit users ability to create new shared drives", + "Checks": [ + "drive_shared_drive_creation_allowed" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Organizations should determine if all users should have the ability to create new shared drives.", + "RationaleStatement": "Organizations should allow users to create shared drives only if it is allowed under your organization's DLP restrictions and meets your organization's requirements. By default, when a user account is deleted all the data in their personal drive is deleted as well. In this case, administrators should transfer ownership of a user's files to another user. Note: See additional information on transferring files.", + "ImpactStatement": "Disabling this feature will prevent users from creating new shared drives.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Shared drive creation 6. Set Prevent users in from creating new shared drives to unchecked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Under Sharing settings, select Shared drive creation 6. Ensure Prevent users in from creating new shared drives is un-checked", + "AdditionalInformation": "To transfer ownership of a user's files via the Google Workspace Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Transfer ownership 6. Under From user, enter the user's organizational email address that needs to be transferred 7. Under To user, enter the user's organizational email address that is receiving ownership of the files Transfer Drive files to a new owner as an admin", + "DefaultValue": "Prevent users in from creating new shared drives is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.2.2", + "Description": "Ensure manager access members cannot modify shared drive settings", + "Checks": [ + "drive_shared_drive_managers_cannot_override" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Only administrators should be able to modify shared drive settings.", + "RationaleStatement": "Allowing manager access members to override or modify shared drive settings can allow intentional and unintentional data access by unauthorized users.", + "ImpactStatement": "Disabling this feature will prevent manager access members from modifying shared drive settings, requiring administrators to perform settings modifications as required.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Sharing settings 6. Under Shared drive creation, set Allow members with manager access to override the settings below to unchecked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Sharing settings 6. Under Shared drive creation, ensure Allow members with manager access to override the settings below is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Allow members with manager access to override the settings below is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.2.3", + "Description": "Ensure shared drive file access is restricted to members only", + "Checks": [ + "drive_shared_drive_members_only_access" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Shared drive file access should be restricted to that shared drive's members", + "RationaleStatement": "Preventing unauthorized users from access sensitive data is paramount in preventing unauthorized or unintentional information disclosures.", + "ImpactStatement": "Disabling this feature will prevent shared drive non-members from accessing content in shared drives where they are not a member.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Sharing settings 6. Under Shared drive creation, set Allow people who aren't shared drive members to be added to files to unchecked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Sharing settings 6. Under Shared drive creation, ensure Allow people who aren't shared drive members to be added to files is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Allow people who aren't shared drive members to be added to files is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.1.2.4", + "Description": "Ensure 'Download, print, and copy is enabled for' is not set to 'Everyone (Managers, content managers, contributors, commenters and viewers)'", + "Checks": [ + "drive_shared_drive_disable_download_print_copy" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "limit what viewers/commenters on a shared document can do with it.", + "RationaleStatement": "In many cases when sharing a document it might be fine for the users to do what they want with the document on the shared drive (Download, Print, etc.). In more restricted environments these capabilities may need to be prevented (Protected Intellectual property, Personally Identifiable Information, etc.).", + "ImpactStatement": "Users of this shared drive will be restricted to only reading and commenting on the existing files.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Sharing settings 6. Under Shared drive creation, set Download, print, and copy is enabled for to not Everyone (Managers, content managers, contributors, commenters and viewers) 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Sharing settings 6. Under Shared drive creation, ensure Download, print, and copy is enabled for is not set to Everyone (Managers, content managers, contributors, commenters and viewers)", + "AdditionalInformation": "", + "DefaultValue": "Allow viewers and commenters to download, print, and copy files is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.2.1", + "Description": "Ensure offline access to documents is disabled", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Prevent documents from being locally accessible on an unconnected device.", + "RationaleStatement": "This setting prevents an organization's files from being stored locally, thus limiting data loss issues if the device is lost or stolen.", + "ImpactStatement": "Copies of recent files are only synced and saved on devices if you've defined a managed policy to do so. NOTE: All users will lose access to offline documents on all devices if managed devices policies are not set. NOTE: Setting up policies to control offline access on individual devices is outside the scope of this Benchmark. Additional information om doing this for various device types can be found here.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Features and Applications 6. Select Offline 7. Set Control offline access using device policies. to checked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Features and Applications 6. Select Offline 7. Ensure Control offline access using device policies is checked", + "AdditionalInformation": "", + "DefaultValue": "Control offline access using device policies is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.2.3.1", + "Description": "Ensure desktop access to Drive is disabled", + "Checks": [ + "drive_desktop_access_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.2 Drive and Docs", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Prevent documents from being locally accessible on an unconnected device.", + "RationaleStatement": "This setting prevents an organization's files from being stored locally, thus limiting data loss issues if the device is lost or stolen. NOTE: The Google Drive desktop application has its own way of handling \"Offline\" files and does not obey the Drive and Doc > Offline > Control offline access using divide policies setting. Not allowing Google Drive for desktop on the device will prevent this channel.", + "ImpactStatement": "The end user will not be able to use Google Drive for desktop and its convenient integration into the Windows file explorer.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Google Drive for desktop 6. Set Allow Google Drive for desktop in your organization to unchecked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Drive and Docs 5. Select Google Drive for desktop 6. Ensure Allow Google Drive for desktop in your organization is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Allow Google Drive for desktop in your organization is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.1.1", + "Description": "Ensure users cannot delegate access to their mailbox", + "Checks": [ + "gmail_mail_delegation_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Mail delegation allows the delegate to read, send, and delete messages on their behalf. For example, a manager can delegate Gmail access to another person in their organization, such as an administrative assistant.", + "RationaleStatement": "Only administrators should be able to delegate access to a user's mailboxes.", + "ImpactStatement": "Existing delegations will be hidden, when this feature is disabled.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under User Settings - Mail delegation, set Let users delegate access to their mailbox to other users in the domain to unchecked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under User Settings - Mail delegation, ensure Let users delegate access to their mailbox to other users in the domain is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Let users delegate access to their mailbox to other users in the domain is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.1.2", + "Description": "Ensure offline access to Gmail is disabled", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Disables the user's ability to utilize various Gmail functions (read, write, search, delete, and label email messages) while not connected to the internet.", + "RationaleStatement": "Prevents the organization's data (user's email) from being copied to remote computers.", + "ImpactStatement": "Users will need internet access to use Gmail.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Gmail 4. Select User Settings 5. SelectGmail web offline 6. Set Enable Gmail web offline to unchecked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Gmail 4. Select User Settings 5. Under Gmail web offline 6. Ensure Enable Gmail web offline is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Enable Gmail web offline is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.2.1", + "Description": "Ensure that DKIM is enabled for all mail enabled domains", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "DKIM adds an encrypted signature to the header of all outgoing messages. Email servers that get signed messages use DKIM to decrypt the message header, and verify the message was not changed after it was sent.", + "RationaleStatement": "Spoofing is a common unauthorized use of email, so some email servers require DKIM to prevent email spoofing.", + "ImpactStatement": "There should be no impact of setting up DKIM however, organizations should ensure appropriate setup to ensure continuous mail-flow.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Authenticate email, select - Generate new record 6. Under Select DKIM key bit length, select the appropriate key bit length 2048 is recommended if supported 7. Under Prefix selector (optional), enter the appropriate prefix selector 8. Use the text at TXT record value to update the DNS record at your domain host 9. Select Start Authentication", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Authenticate email, ensure a DKIM record exists for each mail enabled domain", + "AdditionalInformation": "", + "DefaultValue": "None", + "References": "" + } + ] + }, + { + "Id": "3.1.3.2.2", + "Description": "Ensure the SPF record is configured for all mail enabled domains", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "For all the email domains configured in Google Workspace, a corresponding Sender Policy Framework (SPF) record should be created. NOTE: There are a number of ways SPF can be configured, this document presents a most basic method. For more information on setting up SPF for Google Workspace please refer to the Google documentation. • How SPF protects against spoofing and spam • Define your SPF record—Basic setup", + "RationaleStatement": "SPF records allow Gmail and other mail systems to know where messages from your domains are allowed to originate. This information can be used by that system to determine how to treat the message based on if it is being spoofed or is valid.", + "ImpactStatement": "There should be minimal impact of setting up SPF records however, organizations should ensure proper SPF record setup as email could be flagged as spam if SPF is not set up appropriately.", + "RemediationProcedure": "Configure the DNS record for each domain. • If all email in your domain is sent from and received by Google Gmail, add the following TXT record for each domain: v=spf1 include:_spf.google.com ~all NOTE: This will likely need to be configured at your domain registrar (Godaddy, etc.).", + "AuditProcedure": "Check the DNS records for each domain. 1. Use a Domain Name System (DNS) lookup tool to review the current configuration for your domain (DNS Records). This information can be discovered in a variety of ways: o Reviewing the DNS Record information at your domain registrar (GoDaddy, etc.) o Using an OS based nslookup tool on your workstation OS o Using Google Dig tool available from the Google Admin Toolbox site (Link: Dig) 2. Using the chosen tool, enter your email domain name (ex. domain1.com) 3. In the results displayed, ensure that a TXT Record with the value of v=spf1 include:_spf.google.com ~all exists and designates Google Gmail as a authorized sender.", + "AdditionalInformation": "", + "DefaultValue": "None", + "References": "" + } + ] + }, + { + "Id": "3.1.3.2.3", + "Description": "Ensure the DMARC record is configured for all mail enabled domains", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "For all email domains configured in Google Workspace, a corresponding Domain-Based Message Authentication, Reporting and Conformance (DMARC) record should be created. NOTE: There are a number of ways DMARC can be configured, this document presents a most basic method. For more information on setting up DMARC for Google Workspace please refer to the Google documentation. • Help prevent spoofing and spam with DMARC • Tutorial: Recommended DMARC rollout", + "RationaleStatement": "DMARC works with Sender Policy Framework (SPF) and Domain Keys Identified Mail (DKIM) to authenticate mail senders and ensure that destination email systems trust messages sent from your domain. Spammers can spoof your domain or organization to send fake messages that impersonate your organization. DMARC tells receiving mail servers what to do when they get a message that appears to be from your organization, but doesn't pass authentication checks, or doesn’t meet the authentication requirements in your DMARC policy record. Messages that aren't authenticated might be impersonating your organization, or might be sent from unauthorized servers.", + "ImpactStatement": "There should be minimal impact of setting up DMARC records however, organizations should ensure proper DMARC record setup as email could be flagged as spam if DMARC is not set up appropriately.", + "RemediationProcedure": "Configure the DNS record for each domain. 1. If all email in your domain is sent from and received by Google Gmail, add the following TXT record for the domain: v=DMARC1; p=none; rua=mailto: NOTE: This will likely need to be configured at your domain registrar (Godaddy, etc.).", + "AuditProcedure": "Check the DNS records for each domain. 1. Use a Domain Name System (DNS) lookup tool to review the current configuration for your domain (DNS Records). This information can be discovered in a variety of ways: o Reviewing the DNS Record information at your domain registrar (GoDaddy, etc.) o Using an OS based nslookup tool on your workstation OS o Preferred: Using Google Dig tool available from the Google Admin Toolbox site (Link: Dig) 2. Using the chosen tool, enter your email domain name (ex. domain1.com) 3. In the results displayed, ensure that a TXT Record with the value of v=DMARC1; p=none; rua=mailto: exists. This designates Google Gmail as an authorized sender. NOTE: The p=none sets DMARC to non-enforcing. This is a relaxed DMARC policy that lets you start getting reports without risking messages from your domain being rejected or marked as spam by receiving servers. Start with a none policy that only monitors email flow, and then eventually change to a policy that rejects all unauthenticated messages (p=reject). NOTE: The rua=mailto:_report@domain1.com entry is optional but setting it to a valid email address is recommended. RUA reports provide a comprehensive view of all of a domain’s traffic. At a minimum, organizations should configure their DMARC record to receive RUA reports. The Difference in DMARC Reports: RUA and RUF", + "AdditionalInformation": "", + "DefaultValue": "None", + "References": "" + } + ] + }, + { + "Id": "3.1.3.3.1", + "Description": "Enable quarantine admin notifications for Gmail", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Quarantines can help prevent spam, minimize data loss, and protect confidential information. They can also help moderate message attachments so users don’t send, open, or click something they shouldn’t.", + "RationaleStatement": "Admins should be notified periodically when messages are quarantined so they can take the appropriate actions.", + "ImpactStatement": "Admins will begin receiving quarantine notifications as emails are quarantined.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Manage quarantines, set Notify periodically when messages are quarantined to checked As required, give appropriate users the Access Admin Quarantine and\\or Access restricted quarantine roles", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Manage quarantines, ensure each quarantine has Notify periodically when messages are quarantined is checked", + "AdditionalInformation": "", + "DefaultValue": "Notify periodically when messages are quarantined is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.1.1", + "Description": "Ensure protection against encrypted attachments from untrusted senders is enabled", + "Checks": [ + "gmail_encrypted_attachment_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "As a Google Workspace administrator, you can protect incoming mail against phishing and harmful software (malware). You can also choose what action to take based on the type of threat detected.", + "RationaleStatement": "You should protect your users from potentially malicious attachments.", + "ImpactStatement": "Users will be warned when they receive an encrypted attachment from an untrusted sender.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Attachments, set Protect against encrypted attachments from untrusted senders to checked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Attachments, ensure Protect against encrypted attachments from untrusted senders is checked", + "AdditionalInformation": "", + "DefaultValue": "Protect against encrypted attachments from untrusted senders is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.1.2", + "Description": "Ensure protection against attachments with scripts from untrusted senders is enabled", + "Checks": [ + "gmail_script_attachment_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "As a Google Workspace administrator, you can protect incoming mail against phishing and harmful software (malware). You can also choose what action to take based on the type of threat detected.", + "RationaleStatement": "You should protect your users from potentially malicious attachments.", + "ImpactStatement": "Users will be warned when they receive an attachments with scripts from an untrusted sender.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Attachments, set Protect against attachments with scripts from untrusted senders to checked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Attachments, ensure Protect against attachments with scripts from untrusted senders is checked", + "AdditionalInformation": "", + "DefaultValue": "Protect against attachments with scripts from untrusted senders is enabled is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.1.3", + "Description": "Ensure protection against anomalous attachment types in emails is enabled", + "Checks": [ + "gmail_anomalous_attachment_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "As a Google Workspace administrator, you can protect incoming mail against phishing and harmful software (malware). You can also choose what action to take based on the type of threat detected.", + "RationaleStatement": "You should protect your users from potentially malicious attachments.", + "ImpactStatement": "Users will be warned when they receive an anomalous attachment.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Attachments, set Protect against anomalous attachment types in emails to checked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Attachments, ensure Protect against anomalous attachment types in emails is checked", + "AdditionalInformation": "", + "DefaultValue": "Protect against anomalous attachment types in emails is Unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.2.1", + "Description": "Ensure link identification behind shortened URLs is enabled", + "Checks": [ + "gmail_shortener_scanning_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Identify links behind short URLs, and display a warning when you click links to untrusted domains.", + "RationaleStatement": "You should protect your users from potentially malicious links.", + "ImpactStatement": "Users will be warned when they click links to untrusted domains.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Links and external images, set Identify links behind shortened URLs to checked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Links and external images, ensure Identify links behind shortened URLs is checked", + "AdditionalInformation": "", + "DefaultValue": "Identify links behind shortened URLs is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.2.2", + "Description": "Ensure scan linked images for malicious content is enabled", + "Checks": [ + "gmail_external_image_scanning_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Scan linked images for malicious content, and display a warning when you click links to untrusted domains.", + "RationaleStatement": "You should protect your users from potentially malicious links.", + "ImpactStatement": "Users will be warned when they click links to untrusted domains.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Links and external images, set Scan linked images to checked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Links and external images, ensure Scan linked images is checked", + "AdditionalInformation": "", + "DefaultValue": "Scan linked images is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.2.3", + "Description": "Ensure warning prompt is shown for any click on links to untrusted domains", + "Checks": [ + "gmail_untrusted_link_warnings_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Display a warning when you click links to untrusted domains.", + "RationaleStatement": "You should protect your users from potentially malicious links.", + "ImpactStatement": "Users will be warned when they click links to untrusted domains.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Links and external images, set Show warning prompt for any click on links to untrusted domains is checked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Links and external images, ensure Show warning prompt for any click on links to untrusted domains is checked", + "AdditionalInformation": "", + "DefaultValue": "Show warning prompt for any click on links to untrusted domains is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.3.1", + "Description": "Ensure protection against domain spoofing based on similar domain names is enabled", + "Checks": [ + "gmail_domain_spoofing_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Moves domain spoofing emails to spam folder.", + "RationaleStatement": "You should protect your users from domain spoofing emails.", + "ImpactStatement": "Domain spoofed emails will be moved to a user's spam folder.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, set Protect against domain spoofing based on similar domain names to checked 6. Set Action to Move email to spam 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, ensure Protect against domain spoofing based on similar domain names is checked 6. Ensure Action is Move email to spam", + "AdditionalInformation": "", + "DefaultValue": "• Protect against domain spoofing based on similar domain names is checked • Action is Keep email in inbox and show warning (default)", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.3.2", + "Description": "Ensure protection against spoofing of employee names is enabled", + "Checks": [ + "gmail_employee_name_spoofing_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Moves employee spoofing emails to spam folder.", + "RationaleStatement": "You should protect your users from employee spoofing emails.", + "ImpactStatement": "Employee spoofed emails will be moved to a user's spam folder.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, set Protect against spoofing of employee names to checked 6. Set Action to Move email to spam 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, ensure Protect against spoofing of employee names is checked 6. Ensure Action is Move email to spam", + "AdditionalInformation": "", + "DefaultValue": "• Protect against spoofing of employee names = checked • Action = Keep email in inbox and show warning (default)", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.3.3", + "Description": "Ensure protection against inbound emails spoofing your domain is enabled", + "Checks": [ + "gmail_inbound_domain_spoofing_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Moves inbound emails spoofing your domain to spam folder.", + "RationaleStatement": "You should protect your users from inbound company domain spoofing emails.", + "ImpactStatement": "Inbound company domain spoofed emails will be moved to a user's spam folder.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, set Protect against inbound emails spoofing your domain to checked 6. Set Action to Move email to spam 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, ensure Protect against inbound emails spoofing your domain is checked 6. Ensure Action is Move email to spam", + "AdditionalInformation": "", + "DefaultValue": "• Protect against inbound emails spoofing your domain = checked • Action = Keep email in inbox and show warning (default)", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.3.4", + "Description": "Ensure protection against any unauthenticated emails is enabled", + "Checks": [ + "gmail_unauthenticated_email_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Displays a warning when any message is not authenticated (SPF or DKIM).", + "RationaleStatement": "You should protect your users from any emails that aren't authenticated (SPF or DKIM)", + "ImpactStatement": "Emails that aren't authenticated (SPF or DKIM) display a warning message to the recipient.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, set Protect against any unauthenticated emails to checked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, ensure Protect against any unauthenticated emails is checked", + "AdditionalInformation": "", + "DefaultValue": "Protect against any unauthenticated emails = unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.4.3.5", + "Description": "Ensure groups are protected from inbound emails spoofing your domain", + "Checks": [ + "gmail_groups_spoofing_protection_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "If a group receives an email that is spoofing your domain it is sent to the spam folder.", + "RationaleStatement": "You should protect your groups from any emails that spoofing your domain.", + "ImpactStatement": "Emails that are spoofing your domain and are received by a group are sent to the spam folder.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, set Protect your Groups from inbound emails spoofing your domain to checked 6. Set Action to Move email to spam 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under Safety - Spoofing and authentication, ensure Protect your Groups from inbound emails spoofing your domain is checked 6. Ensure Action is set to Move email to spam", + "AdditionalInformation": "", + "DefaultValue": "• Protect against any unauthenticated emails = unchecked • Action = Keep email in inbox and display warning (default)", + "References": "" + } + ] + }, + { + "Id": "3.1.3.5.1", + "Description": "Ensure POP and IMAP access is disabled for all users", + "Checks": [ + "gmail_pop_imap_access_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "POP and IMAP may allow users to access Gmail using legacy or unapproved email clients that do not support modern authentication mechanisms, such as multifactor authentication.", + "RationaleStatement": "Disabling POP and IMAP prevents use of legacy and unapproved email clients with weaker authentication mechanisms that would increase the risk of email account credential compromise.", + "ImpactStatement": "If you have Apple iOS or Android device users in your organization and you turn IMAP off, let them know that they’re no longer syncing Google Workspace mail to the iOS or Android Mail app. They might not get a notification on their device. Additionally, new users can’t manually add the Google Account they use for work or school to the device. If your Google Workspace users want to use desktop clients, such as Microsoft Outlook and Apple Mail, to access their Google Workspace mail, you need to enable POP or IMAP access in the Google Admin console. You can enable access for everyone in your organization or only for users in specific organizational units.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under End User Access - POP and IMAP Access 6. Set Enable IMAP access for all users to unchecked 7. Set Enable POP access for all users to unchecked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under End User Access - POP and IMAP Access 6. Ensure Enable IMAP access for all users is unchecked 7. Ensure Enable POP access for all users is unchecked", + "AdditionalInformation": "", + "DefaultValue": "• Enable IMAP access for all users is checked • Enable POP access for all users is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.5.2", + "Description": "Ensure automatic forwarding options are disabled", + "Checks": [ + "gmail_auto_forwarding_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "You should disable automatic forwarding to prevent users from auto-forwarding mail.", + "RationaleStatement": "In the event that an attacker gains control of an end-user account they could create rules to ex-filtrate data from your environment.", + "ImpactStatement": "Care should be taken before implementation to ensure there is no business need for case-by-case auto-forwarding. Disabling auto-forwarding to remote domains will affect all users and in an organization.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under End User Access - Automatic forwarding, set Allow users to automatically forward incoming email to another address to unchecked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under End User Access - Automatic forwarding, ensure Allow users to automatically forward incoming email to another address is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Allow users to automatically forward incoming email to another address is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.5.3", + "Description": "Ensure per-user outbound gateways is disabled", + "Checks": [ + "gmail_per_user_outbound_gateway_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "A per-user outbound gateway is a mail server, other than the Google Workspace mail servers, that delivers outgoing mail for a user in your domain.", + "RationaleStatement": "Mail sent via external SMTP will circumvent your outbound gateway", + "ImpactStatement": "Care should be taken before implementation to ensure there is no business need for mail sent via external SMTP gateway.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under End User Access - Allow per-user outbound gateways, set Allow users to send mail through an external SMTP server when configuring a \"from\" address hosted outside your email domain to unchecked 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Under End User Access - Allow per-user outbound gateways, ensure Allow users to send mail through an external SMTP server when configuring a \"from\" address hosted outside your email domain is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Allow users to send mail through an external SMTP server when configuring a \"from\" address hosted outside your email domain is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.5.4", + "Description": "Ensure external recipient warnings are enabled", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Gmail adds an image or colored border to external addresses.", + "RationaleStatement": "As an admin for your organization, you can turn alerts on or off for messages that include external recipients (people with email addresses outside of your organization). These alerts help people avoid unintentional replies, and remind them to treat external messages with caution.", + "ImpactStatement": "When this setting is on, Gmail shows warnings (colored boarder) when: • An email thread includes external recipients (not available on iOS). • Replying to a message from an external recipient. • Composing a new message to an external recipient (not available on iOS). Gmail doesn't show a warning if the external recipient is in your organization's Directory, personal Contacts, or other Contacts. Warnings aren't displayed for secondary domain or domain alias addresses.", + "RemediationProcedure": "To configure external recipient warnings are enabled, use the Google Workspace Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select End User Access 6. Select Warn for external recipients 7. Set Highlight any external recipients in a conversation. Warn users before they reply to email with external recipients who aren't in their contacts. to checked 8. Select Save", + "AuditProcedure": "To verify Ensure external recipient warnings are enabled, use the Google Workspace Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select End User Access 6. Under Warn for external recipients, ensure Highlight any external recipients in a conversation. Warn users before they reply to email with external recipients who aren't in their contacts. is ON", + "AdditionalInformation": "", + "DefaultValue": "Highlight any external recipients in a conversation. Warn users before they reply to email with external recipients who aren't in their contacts. is ON", + "References": "" + } + ] + }, + { + "Id": "3.1.3.6.1", + "Description": "Ensure enhanced pre-delivery message scanning is enabled", + "Checks": [ + "gmail_enhanced_pre_delivery_scanning_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enables improved detection of suspicious content prior to delivery.", + "RationaleStatement": "As an administrator, you can increase Gmail's ability to identify suspicious content with enhanced pre-delivery message scanning. Typically, when Gmail identifies a possible phishing message, a warning is displayed and the message might be moved to spam.", + "ImpactStatement": "With the Enhanced pre-delivery message scanning option, when Gmail detects suspicious content, message delivery is slightly delayed so that Gmail can do additional security checks on the message.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Spam, phishing, and malware 6. Select Enhanced pre-delivery message scanning. 7. Set Enables improved detection of suspicious content prior to delivery to checked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Spam, phishing, and malware 6. Ensure Enhanced pre-delivery message scanning. is ON", + "AdditionalInformation": "", + "DefaultValue": "Enhanced pre-delivery message scanning. is ON", + "References": "" + } + ] + }, + { + "Id": "3.1.3.6.2", + "Description": "Ensure spam filters are not bypased for internal senders", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "You can configure your advanced Gmail settings to bypass, or not bypass, spam filters for messages received from internal senders.", + "RationaleStatement": "Turning off this setting reduces the risk of spoofing and phishing/whaling.", + "ImpactStatement": "Your users will be better protected by filtering their email for spam and minimizing the chances for spoofing and phishing/whaling attacks.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Spam, phishing, and malware 6. Under Spam, select Configure 7. Set Bypass spam filters for messages received from internal senders. to unchecked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Spam, phishing, and malware 6. Under Spam, select Configure 7. Ensure Bypass spam filters for messages received from internal senders. is unchecked", + "AdditionalInformation": "", + "DefaultValue": "Bypass spam filters for messages received from internal senders. is checked", + "References": "" + } + ] + }, + { + "Id": "3.1.3.7.1", + "Description": "Ensure comprehensive mail storage is enabled", + "Checks": [ + "gmail_comprehensive_mail_storage_enabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Comprehensive mail storage ensures messages sent by other core services appear in users' sent folders and are therefore accessible to Vault.", + "RationaleStatement": "As an administrator, you can ensure that a copy of all sent or received messages in your domain—including messages sent or received by non-Gmail mailboxes—is stored in the associated users' Gmail mailboxes.", + "ImpactStatement": "There are some important considerations to carefully review before enabling comprehensive mail storage: You should not enable comprehensive mail storage if you have compliance routing rules that change the recipient (and don’t want the original recipient to receive a copy of the email). When you have the SMTP Relay service enabled, user mailboxes will keep a copy of the message in the sent folder (for example, when sending mail from a scanner) if comprehensive mail storage is enabled. This might cause accounts to exceed storage limits if your account's edition has storage limits. Compare editions. You should enable comprehensive mail storage if you only use Gmail for the Vault feature and forward email to your on-premise mail server or other email provider.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Compliance 6. Select Comprehensive mail storage 7. Set Ensure that a copy of all sent and received mail is stored in associated users' mailboxes to checked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Compliance 6. Under Comprehensive mail storage, ensure Ensure that a copy of all sent and received mail is stored in associated users' mailboxes is ON", + "AdditionalInformation": "", + "DefaultValue": "Copy of all sent and received mail is stored in associated users' mailboxes is OFF", + "References": "" + } + ] + }, + { + "Id": "3.1.3.7.2", + "Description": "Ensure 'Send email over a secure TLS connection' Is Enabled", + "Checks": [], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.3 Gmail", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The default is that Gmail always tries to send messages over a secure TLS connection. If the receiving server doesn't use TLS, Gmail still sends messages with TLS but the connection isn't secure. This setting allows the option to require a CA-signed certificate, verify the hostname associated with the certificate, and test the TLS connection. A padlock image will appear next to the recipient address if the message will be sent with TLS. The padlock shows only for accounts with a Google Workspace subscription that supports S/MIME encryption. Google Workspace supports TLS versions 1.0, 1.1, 1.2, and 1.3.", + "RationaleStatement": "Transport Layer Security (TLS) encrypts email messages for security and privacy and prevents unauthorized access of messages when they're sent over internet connections.", + "ImpactStatement": "This should not have an impact on the usage of Gmail.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Compliance 6. Select Secure transport (TLS) compliance 7. Select Configure 8. Set Inbound - all messages and Outbound - all messages to checked 9. Select Save Note: Enabling the Inbound - all messages and Outbound - all messages configurations will also, by default, enable Require CA-signed certificate when delivering outbound messages to the TLS-enabled domains specified above. This is not a required configuration, but it is recommended.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Gmail 5. Select Compliance 6. Under Secure transport (TLS) compliance, select Configure 7. Under Email messages to affect ensure Inbound - all messages and Outbound - all messages are ON", + "AdditionalInformation": "", + "DefaultValue": "", + "References": "https://support.google.com/a/answer/2520500" + } + ] + }, + { + "Id": "3.1.4.1.1", + "Description": "Ensure external filesharing in Google Chat and Hangouts is disabled", + "Checks": [ + "chat_external_file_sharing_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.4 Google Chat", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control how files are shared externally in Google Chat and Hangouts.", + "RationaleStatement": "Files often contain confidential information, and some organizations, particularly in regulated industries, need to control the flow of this information within and outside of their organization.", + "ImpactStatement": "Users will not be able to share files via chat externally.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat File Sharing 5. Under Setting, set External filesharing to No files 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat File Sharing 5. Under Setting, verify External filesharing is set to No files", + "AdditionalInformation": "", + "DefaultValue": "External filesharing is Allow all files", + "References": "" + } + ] + }, + { + "Id": "3.1.4.1.2", + "Description": "Ensure internal filesharing in Google Chat and Hangouts is disabled", + "Checks": [ + "chat_internal_file_sharing_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.4 Google Chat", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Control how files are shared internally in Google Chat and Hangouts.", + "RationaleStatement": "Files often contain confidential information, and some organizations, particularly in regulated industries, need to control the flow of this information within and outside of their organization.", + "ImpactStatement": "Users will not be able to share files via chat internally.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat File Sharing 5. Under Setting, set Internal filesharing to No files 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat File Sharing 5. Under Setting, verify Internal filesharing is set to No files", + "AdditionalInformation": "", + "DefaultValue": "Internal filesharing is Allow all files", + "References": "" + } + ] + }, + { + "Id": "3.1.4.2.1", + "Description": "Ensure Google Chat externally is restricted to allowed domains", + "Checks": [ + "chat_external_messaging_restricted" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.4 Google Chat", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control how users chat with people outside of your organization. If you allow your users to chat externally, you can also allow them to create and join spaces with people outside your organization.", + "RationaleStatement": "Restricting external chat to only approved domains potentially limits the spread of company information.", + "ImpactStatement": "Users will not be able to chat with users in any external domain, only approved domains. This will require some admin-level approval and allowlist maintenance.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select External Chat Settings 5. Select Chat externally 6. Set Allow users to send messages outside to ON 7. Set Only allow this for allowlisted domains to checked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select External Chat Settings 5. Select Chat externally 6. Verify Allow users to send messages outside is ON 7. Verify Only allow this for allowlisted domains is checked", + "AdditionalInformation": "", + "DefaultValue": "• Allow users to send messages outside is set to ON • Only allow this for allowlisted domains is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.4.3.1", + "Description": "Ensure external spaces in Google Chat and Hangouts are restricted", + "Checks": [ + "chat_external_spaces_restricted" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.4 Google Chat", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control whether users can create or join spaces within your organization that include external people outside of your organization.", + "RationaleStatement": "Restricting external spaces to only approved domains potentially limits the spread of company information.", + "ImpactStatement": "Users with this setting turned off or who have editions that don't support external spaces can't create these spaces, but they can join existing spaces with external people", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select External Spaces 5. Under Setting, set Allow users at to create and join spaces with people outside their organization to ON 6. Set Only allow users to add people from allowlisted domains to checked 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select External Spaces 5. Under Setting, verify Allow users at to create and join spaces with people outside their organization is ON 6. Verify Only allow users to add people from allowlisted domains is checked", + "AdditionalInformation": "", + "DefaultValue": "• Allow users at to create and join spaces with people outside their organization is ON • Only allow users to add people from allowlisted domains is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.4.4.1", + "Description": "Ensure allow users to install Chat apps is disabled", + "Checks": [ + "chat_apps_installation_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.4 Google Chat", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control the use of Chat apps in spaces or direct messages to connect to services in Google Chat and look up information, schedule meetings, or complete tasks. Apps are accounts created by Google, users in your organization, or third parties.", + "RationaleStatement": "When a user interacts with an app in Chat, the app can see the user's email address, avatar, other basic user information, user locale, timezone, and interaction information. The app can also see the basic user information of other people in the chat, but it can't see their email address or avatar unless they also interact directly with the app. Chat apps that you install from the Google Workspace Marketplace can be made by developers from outside of your organization. Using these Chat app need to be carefully controlled (vetted and approved) since a malicious Chat app could allow the exfiltration of company proprietary information.", + "ImpactStatement": "By default users will not be able to install Chat apps.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat apps 5. Under Chat apps access settings, set Allow users to install Chat apps to OFF 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat apps 5. Under Chat apps access settings, verify Allow users to install Chat apps is OFF", + "AdditionalInformation": "", + "DefaultValue": "Allow users to install Chat apps is ON", + "References": "https://developers.google.com/chat/concepts/apps" + } + ] + }, + { + "Id": "3.1.4.4.2", + "Description": "Ensure allow users to add and use incoming webhooks is disabled", + "Checks": [ + "chat_incoming_webhooks_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.4 Google Chat", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Allow users to configure incoming webhooks and developers to call incoming webhooks to post content. Incoming webhooks let you send asynchronous messages into Google Chat from applications that aren't Chat apps.", + "RationaleStatement": "Webhook usage should be carefully controlled (vetted and approved) since a malicious application could send bogus information to exposed webhooks and ultimately these users.", + "ImpactStatement": "By default users will have exposed webhooks.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat apps 5. Under Chat apps access settings, set Allow users to add and use incoming webhooks to OFF", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Chat and classic Hangouts 4. Select Chat apps 5. Under Chat apps access settings, verify Allow users to add and use incoming webhooks is OFF", + "AdditionalInformation": "", + "DefaultValue": "Allow users to add and use incoming webhooks is ON", + "References": "https://developers.google.com/chat/concepts/apps" + } + ] + }, + { + "Id": "3.1.6.1", + "Description": "Ensure accessing groups from outside this organization is set to private", + "Checks": [ + "groups_external_access_restricted" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.6 Groups for Business", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Choose whether people outside your organization can access your groups. Group owners can further restrict access as needed.", + "RationaleStatement": "Who can externally view groups internal to the organization should be carefully controlled and their access vetted as needed.", + "ImpactStatement": "No one outside your organization can view or search for your groups. External users can email the group if group settings allow.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Groups for Business 5. Select Sharing options 6. Set Accessing groups from outside this organization to Private 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Groups for Business 5. Select Sharing options 6. Verify Accessing groups from outside this organization is Private", + "AdditionalInformation": "", + "DefaultValue": "Accessing groups from outside this organization is Private", + "References": "https://support.google.com/a/answer/10308022?hl=en" + } + ] + }, + { + "Id": "3.1.6.2", + "Description": "Ensure creating groups is restricted", + "Checks": [ + "groups_creation_restricted" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.6 Groups for Business", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control who is allowed to create Groups in your organization and if they can have external members.", + "RationaleStatement": "The organization should have some control over the organizational groups created and the purpose they are for.", + "ImpactStatement": "In a large organization, this may cause too much burden on administrators.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Groups for Business 5. Select Creating groups 6. Select Only organization admins can create groups 7. Set Group owners can allow external members Organization admins can always add external members to unchecked 8. Set Group owners can allow incoming email from outside the organization to unchecked 9. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Groups for Business 5. Select Creating groups 6. Verify Only organization admins can create groups is selected 7. Verify Group owners can allow external members Organization admins can always add external members is unchecked 8. Verify Group owners can allow incoming email from outside the organization is unchecked", + "AdditionalInformation": "", + "DefaultValue": "• Anyone in the organization can create groups is selected • Group owners can allow external members Organization admins can always add external members is unchecked • Group owners can allow incoming email from outside the organization is unchecked", + "References": "" + } + ] + }, + { + "Id": "3.1.6.3", + "Description": "Ensure default for permission to view conversations is restricted", + "Checks": [ + "groups_view_conversations_restricted" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.6 Groups for Business", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "By default, only allow group members to view group conversations.", + "RationaleStatement": "Conversation viewing can always be expanded by exception for certain groups as needed (Need to know), but by default be restricted.", + "ImpactStatement": "No practical impact, since Group members can view conversations in the Group.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Groups for Business 5. Select Sharing options 6. Set Default for permission to view conversations to All group members 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Groups for Business 5. Select Sharing options 6. Verify Default for permission to view conversations is All group members", + "AdditionalInformation": "", + "DefaultValue": "Default for permission to view conversations is All organization users", + "References": "" + } + ] + }, + { + "Id": "3.1.7.1", + "Description": "Ensure service status for Google Sites is set to off", + "Checks": [ + "sites_service_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.7 Sites", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "By default turn off Google Sites for all users.", + "RationaleStatement": "There is really no reason for every user within an organization to have access to Google Sites. If this capability is needed, it can be enabled and configured for those users and groups by exception as required by the organization to meet specific needs.", + "ImpactStatement": "Users will not be have access to Google Sites.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Sites 5. Select Service status 6. Set Service status to OFF for everyone 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select Sites 5. Select Service status 6. Verify Service status is OFF for everyone", + "AdditionalInformation": "", + "DefaultValue": "Service status is ON for everyone", + "References": "" + } + ] + }, + { + "Id": "3.1.8.1", + "Description": "Ensure access to external Google Groups is OFF for Everyone", + "Checks": [ + "additionalservices_external_groups_disabled" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.8 Additional Google services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Control whether users in your organization can access external groups from their Google Workspace account. External groups are created outside your organization and might include a public community group or a group for a club a user belongs to. Control access to external groups by turning on or off the Google Groups additional service — a legacy service in your Admin console that does only one thing: It allows or blocks users from accessing external groups from their Google Workspace account. NOTE: This service has no effect on your organization's internal groups.", + "RationaleStatement": "In general, most of the organization's personnel do not need to assess external groups. They can be allowed by exception as needed by the business.", + "ImpactStatement": "Users can't access external groups from their Google Workspace account. However, they do continue to receive email digests from groups they're already subscribed to when you turn off the service.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select `Additional Google services 5. Scroll down to Google Groups 6. Set it to OFF for everyone 7. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace 4. Select `Additional Google services 5. Scroll down to Google Groups 6. Verify it is OFF for everyone", + "AdditionalInformation": "", + "DefaultValue": "Google Groups is ON for Everyone", + "References": "" + } + ] + }, + { + "Id": "3.1.9.1.1", + "Description": "Ensure users access to Google Workspace Marketplace apps is restricted", + "Checks": [ + "marketplace_apps_access_restricted" + ], + "Attributes": [ + { + "Section": "3 Apps", + "SubSection": "3.1.9 Google Workspace Marketplace", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Restrict what Google Marketplace apps a user can install.", + "RationaleStatement": "Users should only be allowed to install approved and vetted apps. This will limit the overall attack surface for the organization.", + "ImpactStatement": "Users can only install approved Google Marketplace apps. This list will have to be created and maintained.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace Marketplace apps 4. Select Settings 5. Under Manage Google Workspace Marketplace allowlist access, set Settings to install third-party Google Workspace Marketplace apps: to Allow users to install and run only selected apps from the Marketplace 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Apps 3. Select Google Workspace Marketplace apps 4. Select Settings 5. Under Manage Google Workspace Marketplace allowlist access, verify Settings to install third-party Google Workspace Marketplace apps: is set to Allow users to install and run only selected apps from the Marketplace", + "AdditionalInformation": "", + "DefaultValue": "Settings to install third-party Google Workspace Marketplace apps: is Allow users to install and run any app from the Marketplace", + "References": "" + } + ] + }, + { + "Id": "4.1.1.1", + "Description": "Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users in administrative roles", + "Checks": [ + "security_2sv_enforced" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enforce 2-Step Verification (Multi-Factor Authentication) for all users assigned administrative roles. These include roles such as: • Help Desk Admin • Groups Admin • Super Admin • Services Admin • User Management Admin • Mobile Admin • Android Admin • Custom Admin Roles", + "RationaleStatement": "Add an extra layer of security to users accounts by asking users to verify their identity when they enter a username and password. 2-Step Verification (Multi-factor authentication) requires an individual to present a minimum of two separate forms of authentication before access is granted. 2-Step Verification provides additional assurance that the individual attempting to gain access is who they claim to be. With 2-Step Verification, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.", + "ImpactStatement": "Implementation of 2-Step Verification (multi-factor authentication) for all users in administrative roles will necessitate a change to user routine. All users in administrative roles will be required to enroll in 2-Step Verification using using phone, SMS, or an authentication application. After enrollment, use of 2-Step Verification will be required for future access to the environment.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Go to Security and click on 2-Step Verification 3. Select the appropriate group with ALL ADMIN ROLES -- Create this group if needed 4. Under Authentication, set Allow users to turn on 2-Step Verification to checked 5. Set Enforcement to On 6. Set New user enrollment period is set to 2 weeks 7. Under Frequency, set Allow user to trust device to unchecked 8. Under Methods, set Any except verification codes via text, phone call to selected 9. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Go to Security and click on 2-Step Verification 3. Select the appropriate group with ALL ADMIN ROLES -- Create this group if needed 4. Under Authentication, ensure Allow users to turn on 2-Step Verification is checked 5. Ensure Enforcement is set to On 6. Ensure New user enrollment period is set to 2 weeks 7. Under Frequency, ensure Allow user to trust device is unchecked 8. Under Methods, ensure Any except verification codes via text, phone call is selected", + "AdditionalInformation": "", + "DefaultValue": "• Allow users to turn on 2-Step Verification is checked • Enforcement is Off • New user enrollment period is None • Frequency - Allow user to trust device is checked • Methods is Any", + "References": "" + } + ] + }, + { + "Id": "4.1.1.2", + "Description": "Ensure hardware security keys are used for all users in administrative roles and other high-value accounts", + "Checks": [ + "security_2sv_hardware_keys_admins" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "A hardware security key connects to a user's device using USB (A & C), Lightning, NFC, or Bluetooth connection. Also, many Android phones and Apple iPhones have built-in security keys accessible via Bluetooth and that can be assigned to a Google Workspace account. The purpose of a physical security key is to provide an additional security layer to high value accounts; in the event of a compromise of a user's credentials (username and password) without the associated security key, the authentication process cannot be successfully completed.", + "RationaleStatement": "The purpose of a physical security key is to provide an additional security layer to high value accounts; in the event of a compromise of a user's credentials (username and password) without the associated security key, the authentication process cannot be successfully completed. Hardware security keys help to protect high value accounts from targeted attacks, including phishing attempts. Adding a hardware security key requirement to your Google privileged accounts adds another layer of depth of protection greater than any other form of two-factor authentication.", + "ImpactStatement": "Users with hardware security keys enabled will need to have physical access to the hardware key in order complete the authentication process and this will force users to adopt a practice of making sure that the physical key is available to them at any point in time that they need to be able to log in. If a hardware security key is lost or stolen, the impacted user can gain access to their Google account by using a backup MFA process and then remove the lost/stolen key and add another one. If a hardware security key is stolen, the user's account is not automatically compromised as the hardware key works in conjunction with the user's account credentials (username & password).", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Go to Security and click on Authentication 3. Under Authentication, select 2-Step Verification 4. Select the option to Allow users to turn on 2-Step Verification 5. Under Enforcement, enable either 'On' or else 'On from' and configure a valid date 6. Under Methods, select Only security key to force the use of a security key 7. Under 2-Step Verification policy suspension grace period, select 1 day 8. Under Security codes, select Don't allow users to generate security codes 9. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Go to Security and click on Authentication 3. Under Authentication, select 2-Step Verification 4. Ensure the option to Allow users to turn on 2-Step Verification is checked 5. Ensure that the Enforcement option is set to either 'On' or 'On from' with a valid date present 6. Under Methods ensure that Only security key is selected 7. Under 2-Step Verification policy suspension grace period ensure that 1 day is selected 8. Under Security codes ensure that Don't allow users to generate security codes is selected", + "AdditionalInformation": "", + "DefaultValue": "• Allow users to turn on 2-Step Verification is checked • Enforcement is Off • New user enrollment period is None • Frequency - Allow user to trust device is checked • Methods is Any", + "References": "https://support.google.com/accounts/answer/6103523?hl=En" + } + ] + }, + { + "Id": "4.1.1.3", + "Description": "Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users", + "Checks": [ + "security_2sv_enforced" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enforce 2-Step Verification (Multi-Factor Authentication) for all users.", + "RationaleStatement": "Add an extra layer of security to users accounts by asking users to verify their identity when they enter a username and password. 2-Step Verification (Multi-factor authentication) requires an individual to present a minimum of two separate forms of authentication before access is granted. 2-Step Verification provides additional assurance that the individual attempting to gain access is who they claim to be. With 2-Step Verification, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.", + "ImpactStatement": "Implementation of 2-Step Verification (multi-factor authentication) for all users will necessitate a change to user routine. All users will be required to enroll in 2-Step Verification using using phone, SMS, or an authentication application. After enrollment, use of 2-Step Verification will be required for future access to the environment.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select 2-Step Verification 4. Under Authentication, check - Allow users to turn on 2-Step Verification 5. Set Enforcement to On 6. Set New user enrollment period to 2 weeks 7. Under Frequency, uncheck - Allow user to trust device 8. Under Methods, select - Any except verification codes via text, phone call 9. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select 2-Step Verification 4. Under Authentication, ensure Allow users to turn on 2-Step Verification is checked 5. Ensure Enforcement is set to On 6. Ensure New user enrollment period is set to 2 weeks 7. Under Frequency, ensure Allow user to trust device is not checked 8. Under Methods, ensure Any except verification codes via text, phone call is selected", + "AdditionalInformation": "", + "DefaultValue": "• Allow users to turn on 2-Step Verification is checked • Enforcement is Off • New user enrollment period is None • Frequency - Allow user to trust device is checked • Methods is Any", + "References": "" + } + ] + }, + { + "Id": "4.1.2.1", + "Description": "Ensure Super Admin account recovery is disabled", + "Checks": [ + "security_super_admin_recovery_disabled" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "This option allows Super Admin users to recover access to their accounts if their password has been forgotten. The option is not available if either Single Sign On or Password Sync is in use.", + "RationaleStatement": "While allowing Super Admins to recover access to their accounts when they have forgotten their passwords reduces the number of support tickets, it also significantly reduces the security of those Super Admin accounts. If a malicious actor has access to the Super Admin's standard user account, they may then have access to the Super Admin account as well.", + "ImpactStatement": "The impact to Super Admins not being allowed to recover their accounts is that there may be downtime while another Super Admin resets their password.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Security. 3. Select Authentication. 4. Under Account recovery select Super admin account recovery. 5. Set Allow super admins to recover their account to unchecked 6. Click Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Security. 3. Select Authentication. 4. Under Account recovery select Super admin account recovery. 5. Ensure Allow super admins to recover their account is unchecked.", + "AdditionalInformation": "", + "DefaultValue": "Allow super admins to recover their account is OFF", + "References": "" + } + ] + }, + { + "Id": "4.1.2.2", + "Description": "Ensure User account recovery is enabled", + "Checks": [ + "security_user_recovery_enabled" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "This option allows non-Super Admin users to recover access to their accounts if their password has been forgotten. The option is not available if either Single Sign On or Password Sync is in use.", + "RationaleStatement": "Allowing users to recover access to their accounts when they have forgotten their passwords reduces the number of support tickets generated by users, and reduces the amount of down time spent waiting on the account recovery process to initiate and complete.", + "ImpactStatement": "The potential impact to users being allowed to recover their accounts includes: 1. The user is now empowered to reset their passwords. 2. The user will no longer need to call a helpdesk or open a support ticket to regain access to their account. An organization that allows users to recover their account will realize less time spent by administrative staff working on these tasks.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Security. 3. Select User account recovery 4. Select either the pencil icon or the setting itself. 5. Set Allow users and non-super admins to recover their account to checked. 6. Select Save.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Security. 3. Select User account recovery 4. Verify Allow users and non-super admins to recover their account is checked.", + "AdditionalInformation": "", + "DefaultValue": "Allow users and non-super admins to recover their account is OFF", + "References": "" + } + ] + }, + { + "Id": "4.1.3.1", + "Description": "Ensure Advanced Protection Program is configured", + "Checks": [ + "security_advanced_protection_configured" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Enable Google's Advanced Protection Platform for all users and prevent the use of security codes where applicable.", + "RationaleStatement": "Sophisticated phishing tactics can trick the most savvy users into giving their sign-in credentials to attackers. Advanced Protection requires you to use a security key, which is a hardware device or special software on your phone used to verify your identity, to sign in to your Google Account. Unauthorized users won’t be able to sign in without your security key, even if they have your username and password. The Advanced Protection Program includes a curated group of high-security policies that are applied to enrolled accounts. Additional policies may be added to the Advanced Protection Program to ensure the protections are current. Advanced Protection allows you to apply all of these protections at once, and override similar settings you may have configured manually. These policies include: Strong authentication with security keys Use of security codes with security keys (as needed) Restrictions on third-party access to account data Deep Gmail scans Google Safe Browsing protections in Chrome (when users are signed into Chrome using the same identity as their Advanced Protection Program identity) Account recovery through admin", + "ImpactStatement": "User Impact You need your security key when you sign in for the first time on a computer, browser, or device. If you stay signed in, you may not be asked to use your security key the next time you log in. Limits third-party app access to your data, puts stronger checks on suspicious downloads, and tightens account recovery security to help prevent unauthorized access. Security Keys - 2 Required Android: With an Android 7.0+ phone, you can enroll in a few taps by registering your phone’s built-in security key. iPhone: If you have an iPhone running iOS 10.0+, install the Google Smart Lock app to register your security key first, then enroll. Two security keys are required for added assurance. If one key is lost or damaged, users can use the second key to regain account access. Third-Party iDP You can use the Advanced Protection Program with accounts that federate from an IdP using SAML. When users with these accounts enroll in the Advanced Protection Program, we’ll require security key use after the user signs in on the IdP. Note that SAML users can select Remember the device to avoid challenges on a browser or device. Security Codes Before allowing users to generate security codes, carefully evaluate if your organization needs them. Using security keys with security codes increases the risk of phishing. However, if your organization has important workflows where security keys can’t be used directly, enabling security codes for those situations may help improve your security posture overall. Using 'Sign in with Google' with other apps and services You can still sign into apps and services with Google. If they request access to your Gmail or Drive data, access is denied.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Advanced Protection Program 4. Under Enrollment - Allow users to enroll in the Advanced Protection Program, set Enable user enrollment to selected for the desired organizational unit or group 5. Under Security Codes, set Do not allow users to generate security codes to selected for the desired organizational unit or group 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Advanced Protection Program 4. Under Enrollment - Allow users to enroll in the Advanced Protection Program, ensure Enable user enrollment is selected for the desired organizational unit or group 5. Under Security Codes, ensure Do not allow users to generate security codes is selected for the desired organizational unit or group", + "AdditionalInformation": "", + "DefaultValue": "• Allow users to enroll in the Advanced Protection Platform is selected • Security codes is Allow security codes without remote access", + "References": "" + } + ] + }, + { + "Id": "4.1.4.1", + "Description": "Ensure login challenges are enforced", + "Checks": [], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Configure Google Workspace to verify a user's identity post-sso.", + "RationaleStatement": "Many organizations use third-party identity providers (IdPs) to authenticate users who use single sign on (SSO) through SAML. The third-party IdP authenticates users and no additional risk-based challenges are presented to them. Any Google 2-Step Verification (2SV) configuration is ignored. This is the default behavior. You can set a policy to allow additional risk-based authentication challenges and 2SV if it’s configured. If Google receives a valid SAML assertion (authentication information about the user) from the IdP during user sign-in, Google can present additional challenges to the user. Login challenges requires users have a recovery phone number or email account associated with their organizational account. If not previously configured, users will be prompted to enter this information periodically until provided. One login challenge option prompts users to enter their employee ID. This method is susceptible to information gathering attacks, should a list of employee IDs ever be leaked.", + "ImpactStatement": "The potential impact associated with implementation of this setting is dependent upon the existing 2-Step Verification (2SV) polices. • If you have existing 2SV policies, such as 2SV enforcement, those policies apply immediately. • Users affected by the new policy and who are enrolled in 2SV get a 2SV challenge at sign-in. • Based on Google sign-in risk analysis, users might see risk-based challenges at sign-in.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Authentication 4. Select Login Challenges 5. Depending on your organization's SSO configuration: o Under Settings for users signing in using the legacy SSO profile set Ask users for additional verifications from Google if a sign-in or session behavior looks suspicious, and always apply 2-Step Verification policies (if configured) to checked o Under Settings for users signing in using other SSO profiles set Ask users for additional verifications from Google if a sign-in or session behavior looks suspicious, and always apply 2-Step Verification policies (if configured) to checked 6. Select Save 7. Under Login challenges, set Use employee ID to keep my users more secure to unchecked 8. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Authentication 4. Select Login Challenges 5. Select Post-SSO verification 6. Depending on your organization's SSO configuration: o Under Settings for users signing in using the legacy SSO profile ensure Ask users for additional verifications from Google if a sign-in or session behavior looks suspicious, and always apply 2-Step Verification policies (if configured) is checked o Under Settings for users signing in using other SSO profiles ensure Ask users for additional verifications from Google if a sign-in or session behavior looks suspicious, and always apply 2-Step Verification policies (if configured) is checked 7. Under Login challenges, ensure Use employee ID to keep my users more secure is unchecked", + "AdditionalInformation": "This recommendation is giving guidance for using Google authentication as your primary SSO. If you are using a third-party SSO, you will need to configure that through either: To configure this setting via the Google Workspace Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Authentication 4. Depending on your organization's SSO and SAML configuration: o Select SSO with SAML applications o Select SSO with third-party IdP You will need to configure either option to your organization's third-party provider's requirements. Your provider should be able to assist you with that configuration, but it is outside the scope of the benchmark.", + "DefaultValue": "• Post-SSO verification is Logins using SSO bypass additional verifications • Use employee ID to keep my users more secure is unchecked", + "References": "" + } + ] + }, + { + "Id": "4.1.5.1", + "Description": "Ensure password policy is configured for enhanced security", + "Checks": [ + "security_password_policy_strong" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.1 Authentication", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configure Google Workspace Password Policy with a more secure length and is enforced upon next sign-in to protect against the use of common password attacks.", + "RationaleStatement": "Strong password policies protect an organization by prohibiting the use of weak passwords.", + "ImpactStatement": "The potential impact associated with implementation of this setting is dependent upon the existing password policies in place in the environment. For environments that have strong password policies in place, the impact will be minimal. For organizations that do not have strong password policies in place, enhancing the password policy may require users to change passwords, and adhere to more stringent requirements than they have been accustomed to. Configuring passwords to expire at a 1 year mark ensures that users are not forced to change passwords so often that easily discerned patterns are used in the creation of the passwords. The day-to-day impact on users will be that they have to manage fewer passwords changing on a frequent basis. NOTE: Password should be changed immediately on any indication of system compromise, when a user role changes, and when a user leaves the organization.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Password management 4. Under Strength, set Enforce strong passwords to checked 5. Under Length, set Minimum Length to 14 or greater 6. Under Strength and Length enforcement, set Enforce password policy at next sign-in is checked 7. Under Reuse, set Allow password reuse to unchecked 8. Under Expiration, set Password reset frequency to 365 Days 9. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Password management 4. Under Strength, ensure Enforce strong passwords is checked 5. Under Length, ensure Minimum Length is set to 14+ 6. Under Strength and Length enforcement, ensure Enforce password policy at next sign-in is set to checked 7. Under Reuse, ensure Allow password reuse is unchecked 8. Under Expiration, ensure Password reset frequency is set to 365 Days", + "AdditionalInformation": "", + "DefaultValue": "• Enforce strong password is checked • Minimum length is 8 • Maximum length is 100 • Enforce password policy at next sign-in is not checked • Allow password reuse is not checked • Expiration is Never expires", + "References": "" + } + ] + }, + { + "Id": "4.2.1.1", + "Description": "Ensure application access to Google services is restricted", + "Checks": [ + "security_app_access_restricted" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Prevent unrestricted application access to Google services.", + "RationaleStatement": "You can restrict (or leave unrestricted) access to most Workspace services, including Google Cloud Platform services such as Machine Learning. For Gmail and Google Drive, you can specifically restrict access to high-risk scopes (for example, sending Gmail or deleting files in Drive). While users are prompted to consent to apps, if an app uses restricted scopes and you haven’t specifically trusted it, users can’t add it.", + "ImpactStatement": "The potential impact associated with implementation of this setting is that any previously installed apps that you haven’t trusted stop working and tokens are revoked. When a user tries to install an app that has a restricted scope, they’re notified that it’s blocked.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls, then select App access control 5. Under Overview, select MANAGE GOOGLE SERVICES 6. Select ALL applicable Google Services 7. Click Change access 8. Select Restricted: Only trusted apps can access a service", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls, then select App access control 5. Under Overview, select MANAGE GOOGLE SERVICES 6. Ensure ALL applicable Google Services have Restricted in the Access column", + "AdditionalInformation": "", + "DefaultValue": "Access is Unrestricted", + "References": "" + } + ] + }, + { + "Id": "4.2.1.2", + "Description": "Review third-party applications periodically", + "Checks": [], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Weekly review connected applications for potential malicious or unintended access or connections.", + "RationaleStatement": "Performing a periodic review of connected applications and their permission scopes ensures only permitted and required applications can access organizational data or resources. Attackers commonly attempt to persuade or trick users to grant their application access to organizational data resources by asking for their consent.", + "ImpactStatement": "Blocking or removing unauthorized third-party applications will immediately revoke their access to organizational data as well as Google Workspace APIs. This reduces the organizations risk exposure.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls, then select App access control 5. Under Overview, select MANAGE THIRD-PARTY APP ACCESS 6. Select Change Access for the application you wish to remove 7. Select Blocked: Can't access any Google service 8. Log in to the Google Cloud Platform - Resource Manager https://console.cloud.google.com/cloud-resource-manager as an administrator 9. Now Delete the desired application", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls, then select App access control 5. Under Overview, select MANAGE THIRD-PARTY APP ACCESS 6. Ensure all listed applications have been properly vetted and authorized by the appropriate personnel", + "AdditionalInformation": "", + "DefaultValue": "None", + "References": "" + } + ] + }, + { + "Id": "4.2.1.3", + "Description": "Ensure internal apps can access Google Workspace APIs", + "Checks": [ + "security_internal_apps_trusted" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enable access to Google Workspace APIs for customer-owned / developed applications.", + "RationaleStatement": "All organization-built internal apps (owned by your organization), can be trusted to access restricted Google Workspace APIs. That way, the organization does not have to trust them all individually.", + "ImpactStatement": "Configuring 'Trusted' status for internal applications allows apps to access Google Workspace APIs. This bypasses individual administrator approval for each usage of the application while maintaining domain-level permissions.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls, then select App access control 5. Under Settings, select Trust internal, domain-owned apps 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls, then select App access control 5. Under Settings, verify Trust internal, domain-owned apps is selected", + "AdditionalInformation": "", + "DefaultValue": "Trust internal, domain-owned apps is selected", + "References": "" + } + ] + }, + { + "Id": "4.2.1.4", + "Description": "Review domain-wide delegation for applications periodically", + "Checks": [], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Weekly review domain-wide delegations for applications for potentially malicious or unintended access or connections.", + "RationaleStatement": "Domain-wide delegation is a powerful feature that allows apps to access users' data across your organization's entire Workspace account. Performing a periodic review of domain-wide delegations for applications and their permission scopes ensures only permitted and required applications can access organizational data or resources.", + "ImpactStatement": "Removing or modifying domain-wide delegation for an application immediately affects its ability to access user data across the entire domain.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls 5. Under Domain wide delegation, select MANAGE DOMAIN WIDE DELEGATION 6. Select Change Access for the application you wish to remove 7. Now Delete the desired application", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select API Controls 5. Under Domain wide delegation, select MANAGE DOMAIN WIDE DELEGATION 6. Ensure all listed applications have been properly vetted and authorized by the appropriate personnel", + "AdditionalInformation": "", + "DefaultValue": "None", + "References": "" + } + ] + }, + { + "Id": "4.2.2.1", + "Description": "Ensure blocking access from unapproved geographic locations", + "Checks": [], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Restrict access to selected Google applications by geographic location. Note: This setting will not be displayed if your organization is using a third-party SSO, ie MS365.", + "RationaleStatement": "Restricting access to known/approved geographic locations is a simple way to limit where attacks can originate from. Especially for smaller organizations that do not need global access to applications.", + "ImpactStatement": "Valid/approved users traveling to a geographic region outside of those defined in the Access Level will not be able to access their applications.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: Create an appropriate Access Level 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Context-Aware Access 5. Select Access levels 6. Select Create Access Level 7. Under Details - Name the Access Level (Suggested using a clear name - ex. \"Restrict to USA\") 8. Under Conditions - Select Basic 9. Under Condition 1 - Select Meet attributes 10. Under Condition 1 - Select Add Attribute 11. Click on the Add Attribute drop-down box and select Geographic origin 12. Click on the far right drop-down box and select the region, or regions, to be allowed (ex. United States) 13. Click Save Assign the defined Access Level has been assigned to the application(s) that need the restriction 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Context-Aware Access 5. Select Assign access levels 6. For each application listed that needs this restriction, select Assign 7. Under, Access is granted when a user meets conditions in at least one of the selected access levels, ensure the previously named Access Level (ex. \"Restrict to USA\") is checked 8. Also, ensure Apply to Google desktop and mobile apps is checked NOTE: CIS recommends geographically restricting assess to the following Google applications at minimum: 1. Admin Console 2. Drives and Docs 3. Gmail 4. Google Vault", + "AuditProcedure": "To verify this setting via the Google Admin Console: Verify an appropriate Access Level has been defined 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Context-Aware Access 5. Select Access levels 6. Review the list of Access Levels displayed and determine if there is an appropriate restriction on geographic access Verify the appropriate Access Level has been assigned to the application(s) that need the restriction 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Context-Aware Access 5. Select Assign access levels 6. Review the list of Google Applications displayed and make sure the appropriate access level for geographic access is assigned to each NOTE: CIS recommends geographically restricting access to the following Google applications at minimum: 1. Admin Console 2. Drives and Docs 3. Gmail 4. Google Vault", + "AdditionalInformation": "", + "DefaultValue": "None", + "References": "" + } + ] + }, + { + "Id": "4.2.3.1", + "Description": "Ensure DLP policies for Google Drive are configured", + "Checks": [ + "security_dlp_drive_rules_configured" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enabling Data Loss Prevention (DLP) policies for Google Drive allows organizations to control the content that users can share in Google Drive files outside the organization.", + "RationaleStatement": "Enabling DLP policies alerts users and administrators that specific types of data should not be exposed, helping to protect the data from accidental exposure. DLP gives you control over what users can share, and prevents unintended exposure of sensitive information such as credit card numbers or identity numbers", + "ImpactStatement": "Configuring a DLP policy for Google Drive will detect or block sensitive information.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Data protection 5. Select Manage Rules 6. Select ADD RULE, then select either New rule or New rule from template New rule Examples can be found here. 1. Set the rule Name 2. Optionally - Set the rule Description 3. Set the Scope as appropriate 4. Select Continue 5. Set Triggers by checking - File modified under Google Drive 6. Select ADD CONDITION and configure values (Field, Comparison Operator, Content to match) - Repeat as appropriate 7. Select Continue 8. Under Actions, select the desired action to take for each incident 9. Under Alerting, select the desired severity level 10. Under Alerting, Select - Send to alert center 11. Select Continue 12. Select Create New rule from template 1. Select the desired rule template 2. Optionally set the Name as desired 3. Optionally set the `Description as desired 4. Set the Scope as appropriate 5. Select Continue 6. Modify preconfigured Conditions as desired, or add additional conditions 7. Select Continue 8. Under Alerting, Select - Send to alert center 9. Select Continue 10. Select Create", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Data protection 5. Select Manage Rules 6. Ensure data protection rules exist and are enabled", + "AdditionalInformation": "", + "DefaultValue": "No DLP policies for Google Drive are configured by default", + "References": "https://support.google.com/a/answer/10846568:https://workspaceupdates.googleblog.com/2020/10/data-protection-dlp-reports.html" + } + ] + }, + { + "Id": "4.2.4.1", + "Description": "Ensure Google session control is configured", + "Checks": [ + "security_session_duration_limited" + ], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configure Google Workspace's session control to strengthen session expiration.", + "RationaleStatement": "As an administrator, you can control how long users can access Google services, such as Gmail on the web, without having to sign in again. For example, for users that work remotely or from untrusted locations, you might want to limit the time that they can access sensitive resources by applying a shorter web session length. If users want to continue accessing a resource when a session ends, they’re prompted to sign in again and start a new session. How the settings work on mobile devices varies by device and app.", + "ImpactStatement": "The potential impact associated with implementation of this setting are: When a web session expires for a user, they see the Verify it's you page and must sign in again. When you change the session length, users need to sign out and in again for settings to take effect. If you set the session to never expire, users never have to sign in again. If you need some users to sign in more frequently than others, place them in different organizational units. Then, apply different session lengths to them. That way, certain users won’t be interrupted to sign in when it isn’t necessary. If a Google Meet meeting starts within 2 hours of a session's scheduled expiration, the user is forced to sign in again before the start of the meeting. This helps avoid an interruption to the meeting while in-progress. If you’re using a third-party identity provider (IdP), such as Okta or Ping, and you set web session lengths for your users, you need to set the IdP session length parameter to expire before the Google session expires. That way, your users will be forced to sign in again. If the third-party IdP session is still valid when the Google session expires, the Google session might be renewed automatically without the user signing in again.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Google session control 5. Set Web session duration to 12 hours or less 6. Select Save", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Google session control 5. Verify Web session duration, is 12 hours or less", + "AdditionalInformation": "", + "DefaultValue": "Web session duration is 14 days", + "References": "" + } + ] + }, + { + "Id": "4.2.5.1", + "Description": "Ensure Google Cloud session control is configured", + "Checks": [], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.2 Access and Data Control", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Configure Google cloud session control to strengthen session expiration.", + "RationaleStatement": "As an administrator, you can control how long different users can access the Google Cloud console and Cloud SDK without having to re-authenticate. For example, you might want users with elevated privileges, like project owners, billing administrators, or others with administrator roles, to re-authenticate more frequently than regular users. If you set a session length, they’re prompted to sign in again to start a new session.", + "ImpactStatement": "The potential impact associated with implementation of this setting are: When a Google cloud session expires for a user, they see the Verify it's you page and must sign in again. If you require a security key, users who do not have one cannot use the GCP Console or Cloud SDK until they set it up. Once they have a security key, they can switch to using their password instead if they want. If you’re using a third-party identity provider (IdP): With the GCP Console—If you require a user to re-authenticate using their password, they’re redirected to the identity provider (IdP). The IdP might not require the user to re-enter their password to start another console session, if the user already has a session active with the IdP—because they are using another application that caused the session to remain active. If a user must re-authenticate by touching their security key, they can do this while using the console. They will not be redirected to the IdP. With the Cloud SDK—If a password is required for re-authentication, gcloud will require the user to execute the gcloud auth login command to renew the session. This will bring up a browser window, and the user will be taken to the IdP, where they may be prompted for credentials if there's no active session with the IdP. If a user must reauthenticate by touching their security key, they can do this on the Cloud SDK. They will not be redirected to the IdP.", + "RemediationProcedure": "To configure this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Google Cloud session control 5. Under Reauthentication policy, set Require reauthentication to selected and Exempt Trusted apps is unchecked 6. Set Reauthentication frequency to 16 hours (recommended) 7. Set Reauthentication method to Security key 8. Select Override", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator 2. Select Security 3. Select Access and Data Control 4. Select Google Cloud session control 5. Under Reauthentication policy, ensure Require reauthentication is selected and Exempt Trusted apps is unchecked 6. Verify Reauthentication frequency, is16 hours (recommended) 7. Verify Reauthentication method is Security key", + "AdditionalInformation": "", + "DefaultValue": "Reauthentication policy is Never require reauthentication", + "References": "" + } + ] + }, + { + "Id": "4.3.1", + "Description": "Ensure the Dashboard is reviewed regularly for anomalies", + "Checks": [], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.3 Security Center", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "As an administrator, you can use the security dashboard to see an overview of different security reports. By default, each security report panel displays data from the last 7 days. You can customize the dashboard to view data from Today, Yesterday, This week, Last week, This month, Last month, or Days ago (up to 180 days). Charts/reports available (Minimum, but could be many more depending on account type): • DLP incidents • Top policy incidents • Failed device password attempts • Compromised device events • Suspicious device activities • OAuth scope grants by product (beta customers only) • OAuth grant activity • OAuth grants to new apps • User login attempts – Challenge method • User login attempts – Failed • User login attempts – Suspicious Details on what each of these charts/reports mean can be found here. This report should be reviewed weekly. NOTE: The availability of each individual report on the security dashboard depends on your Google Workspace edition. See Google documentation for more details. NOTE: In larger organizations reviewing this entire report weekly may not be possible. At a minimum, all Administrator and Super Administrator users should be reviewed, since they are a higher risk. These can be filtered from the overall user list.", + "RationaleStatement": "The Security report provides a comprehensive view of how people share and access data and whether they take appropriate security precautions. For example, you can review who installs external apps, shares numerous files, skips 2-Step Verification, and uses security keys.", + "ImpactStatement": "No user impact.", + "RemediationProcedure": "The remediation for any anomalies in the various fields varies widely (different sections of the Google Workspace Admin UI). Please refer to Google's documentation for specifics (here). NOTE: Many of these settings will be remedied by implementing other sections of this Benchmark. For example, an Admin not enrolled in 2-Step Verification can be remedied by implementing the Remediation procedure for the recommendation Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users in administrative roles.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Reporting 3. Select Reports 4. Select User Reports 5. Select Security, and a table of results will be displayed with the fields listed in the Recommendation description above. 6. Review the displayed users and values for anomalies", + "AdditionalInformation": "", + "DefaultValue": "The report will display all users and fields.", + "References": "https://support.google.com/a/answer/7492330" + } + ] + }, + { + "Id": "4.3.2", + "Description": "Ensure the Security health is reviewed regularly for anomalies", + "Checks": [], + "Attributes": [ + { + "Section": "4 Security", + "SubSection": "4.3 Security Center", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "As an administrator, the security health page enables you to monitor the configuration of your Admin console settings from one location. For example, you can check the status of settings like automatic email forwarding, device encryption, Drive sharing settings, and much more. Settings reported (Minimum, but could be many more depending on account type): • Blocking of compromised mobile devices • Mobile management • Mobile password requirements • Device encryption • Mobile inactivity reports • Auto account wipe • Application verification • Installation of mobile applications from unknown sources • External media storage • Two-step verification for users • Two-step verification for admins • Security key enforcement for admins Details on what each of these report entries mean can be found here. This report should be reviewed weekly. NOTE: The availability of each individual report on the security dashboard depends on your Google Workspace edition. See Google documentation for more details.", + "RationaleStatement": "The security health page provides visibility into your Admin console settings to help you better understand and manage security risks. If needed, you can make adjustments to your domain’s settings based on general security guidelines and best practices, while balancing these guidelines with your organization’s business needs and risk management policy.", + "ImpactStatement": "No user impact.", + "RemediationProcedure": "The remediation for any anomalies in the various settings varies widely (different sections of the Google Workspace Admin UI). Please refer to Google's documentation for specifics (here). NOTE: Many of these settings will be remedied by implementing other sections of this Benchmark. For example, an Admin not enrolled in 2-Step Verification can be remedied by implementing the Remediation procedure for the recommendation Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users in administrative roles.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Security 3. Select Security center 4. Select Security health, and a table of results will be displayed with the settings listed in the Recommendation description above. 5. Review the displayed values for anomalies", + "AdditionalInformation": "", + "DefaultValue": "The report will display the status of a predefined group of settings based on your Google Workspace license.", + "References": "https://support.google.com/a/answer/7491656" + } + ] + }, + { + "Id": "5.1.1.1", + "Description": "Ensure the App Usage Report is reviewed regularly for anomalies", + "Checks": [], + "Attributes": [ + { + "Section": "5 Reporting", + "SubSection": "5.1 Reports", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "As an administrator, you can use Apps usage reports to get an in-depth understanding of how your users use Google Workspace apps. Fields Available: • User • Gmail storage used (MB) • Drive storage used (MB) • Photos storage used (MB) • Total storage used (MB) • Storage used (%) • Classroom - last used time • Classes created • Posts created • Total emails • Emails sent • Emails received • Gmail (IMAP) - last used time • Gmail (POP) - last used time • Gmail (Web) - last used time • Files edited • Files viewed • Drive - last active time • Files added • Other types added • Google Docs added • Google Sheets added • Google Slides added • Google Forms added • Google Drawings added • Posts • +1s • +1s received • Comments • Comments received • Reshares • Reshares received • Search queries • Search queries from web • Search queries from Android • Search queries from iOS Details on what each of these fields mean can be found here. This report should be reviewed weekly. NOTE: In larger organizations reviewing this entire report weekly may not be possible. At a minimum, all Administrator and Super Administrator users should be reviewed, since they are a higher risk. These can be filtered from the overall user list.", + "RationaleStatement": "The App usage report can allow administrator to discover user that are potentially using application that they do not have access to and/or using in atypical ways.", + "ImpactStatement": "No user impact.", + "RemediationProcedure": "The remediation for any anomalies in the various fields varies widely (different sections of the Google Workspace Admin UI). Please refer to Google's documentation for specifics (here). NOTE: Many of these settings will be remedied by implementing other sections of this Benchmark. For example, an Admin showing recent Gmail (IMAP) - last used time and/or Gmail (POP) - last used time can be remedied by implementing the Remediation procedure for the recommendation Ensure POP and IMAP access is disabled for all users.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Reporting 3. Select Reports 4. Select User Reports 5. Select App usage, and a table of results will be displayed with the fields listed in the Recommendation description above. 6. Review the displayed users and values for anomalies", + "AdditionalInformation": "", + "DefaultValue": "The report will display all users and fields.", + "References": "https://support.google.com/a/answer/4579578?hl=en" + } + ] + }, + { + "Id": "5.1.1.2", + "Description": "Ensure the Security Report is reviewed regularly for anomalies", + "Checks": [], + "Attributes": [ + { + "Section": "5 Reporting", + "SubSection": "5.1 Reports", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "As your organization's administrator, you can monitor your users' exposure to data compromise by reviewing the security report. Fields Available: • User • External apps • 2-Step verification enrollment • 2-Step verification enforcement • Password length compliance • Password strength • User account status • Admin status • Security keys enrolled • Less secure apps access • Gmail (IMAP) - last used time • Gmail (POP) - last used time • Gmail (Web) - last used time • External shares • Internal shares • Public • Anyone with link • Outside domain • Anyone in domain shares • Anyone in domain with link shares • Within domain shares • Private shares Details on what each of these fields mean can be found here. This report should be reviewed weekly. NOTE: In larger organizations reviewing this entire report weekly may not be possible. At a minimum, all Administrator and Super Administrator users should be reviewed, since they are a higher risk. These can be filtered from the overall user list.", + "RationaleStatement": "The Security report provides a comprehensive view of how people share and access data and whether they take appropriate security precautions. For example, you can review who installs external apps, shares numerous files, skips 2-Step Verification, and uses security keys.", + "ImpactStatement": "No user impact.", + "RemediationProcedure": "The remediation for any anomalies in the various fields varies widely (different sections of the Google Workspace Admin UI). Please refer to Google's documentation for specifics (here). NOTE: Many of these settings will be remedied by implementing other sections of this Benchmark. For example, an Admin not enrolled in 2-Step Verification can be remedied by implementing the Remediation procedure for the recommendation Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users in administrative roles.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Reporting 3. Select Reports 4. Select User Reports 5. Select Security, and a table of results will be displayed with the fields listed in the Recommendation description above. 6. Review the displayed users and values for anomalies", + "AdditionalInformation": "", + "DefaultValue": "The report will display all users and fields.", + "References": "https://support.google.com/a/answer/6000269?hl=en" + } + ] + }, + { + "Id": "6.1", + "Description": "Ensure User's password changed is configured", + "Checks": [ + "rules_password_changed_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.1", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when a user's password has changed.", + "RationaleStatement": "Ensuring that administrators are alerted when user passwords are changed provides organizations with the ability to detect and halt potential attacks involving credential compromise and account takeover.", + "ImpactStatement": "This setting should have no impact on the end user but will send emails to super administrators when triggered.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to User's password changed and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to Medium 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the User's password changed shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to User's password changed and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to Medium 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "User's password changed is OFF", + "References": "" + } + ] + }, + { + "Id": "6.2", + "Description": "Ensure Government-backed attacks is configured", + "Checks": [ + "rules_government_backed_attacks_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.2", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when Google believes your users are being targeted by a government-backed attack.", + "RationaleStatement": "Ensuring that administrators are alerted that they may be being targeted by a government-backed entity allows them time to check their defenses and potentially up their sensitivity for anomalies. NOTE: Google sends these out of an abundance of caution — the notice does not necessarily mean that the account has been compromised or that there is a widespread attack. Rather, the notice reflects Goggle's assessment that a government-backed attacker has likely attempted to access the user’s account or computer through phishing or malware, for example.", + "ImpactStatement": "This setting should have no impact on the end user but will send emails to super administrators when triggered.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Government-backed attacks and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to High 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the Government-backed attacks shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Government-backed attacks and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to High 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "Government-backed attacks is ON", + "References": "https://support.google.com/a/answer/3230421" + } + ] + }, + { + "Id": "6.3", + "Description": "Ensure User suspended due to suspicious activity is configured", + "Checks": [ + "rules_suspicious_activity_suspension_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.3", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when Google suspended a user's account due to a potential compromise detected.", + "RationaleStatement": "Ensuring that administrators are alerted when the account was suspended by Google. The reason for this should be investigated ASAP, since it could be a possible indication of malicious activity. In any case, the user's account was suspended and something will need to be done to allow the user to resume work.", + "ImpactStatement": "Emails will be sent to all super administrators when triggered. Also, the user's account will be suspended and something will need to be done about that based on company policy (investigated, re-enabled, etc.).", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to User suspended due to suspicious activity and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to High 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the User suspended due to suspicious activity shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to User suspended due to suspicious activity and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to High 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "User suspended due to suspicious activity is ON", + "References": "https://support.google.com/a/answer/3230421" + } + ] + }, + { + "Id": "6.4", + "Description": "Ensure User granted Admin privilege is configured", + "Checks": [ + "rules_admin_privilege_granted_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.4", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when a user has been granted an admin privilege.", + "RationaleStatement": "Ensuring that administrators are alerted when a user is given increased privileges could be an indication of compromise unless this access has been approved.", + "ImpactStatement": "This setting should have no impact on the end user but will send emails to super administrators when triggered.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to User granted Admin privilege and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to Medium 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the User granted Admin privilege shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to User granted Admin privilege and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to Medium 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "User granted Admin privilege is OFF", + "References": "https://support.google.com/a/answer/3230421" + } + ] + }, + { + "Id": "6.5", + "Description": "Ensure Suspicious programmatic login is configured", + "Checks": [ + "rules_suspicious_programmatic_login_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.5", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when Google detects suspicious login attempts from applications or computer programs.", + "RationaleStatement": "Ensuring that administrators are alerted when suspicious login attempts occur. This could be an indication of an active attack on the company by an adversary using previously obtained credentials.", + "ImpactStatement": "This setting should have no impact on the end user but will send emails to super administrators when triggered.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Suspicious programmatic login and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to Low 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the Suspicious programmatic login shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Suspicious programmatic login and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to Low 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "Suspicious programmatic login is ON", + "References": "https://support.google.com/a/answer/3230421" + } + ] + }, + { + "Id": "6.6", + "Description": "Ensure Suspicious login is configured", + "Checks": [ + "rules_suspicious_login_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.6", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when Google detects a sign-in attempt that doesn't match a user's normal behavior, such as a sign-in from an unusual location.", + "RationaleStatement": "Ensuring that administrators are alerted when suspicious login attempts occur. This could be an indication of an active attack on the company by an adversary using previously obtained credentials.", + "ImpactStatement": "This setting should have no impact on the end user but will send emails to super administrators when triggered.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Suspicious login and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to Low 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the Suspicious login shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Suspicious login and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to Low 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "Suspicious login is ON", + "References": "https://support.google.com/a/answer/3230421" + } + ] + }, + { + "Id": "6.7", + "Description": "Ensure Leaked password is configured", + "Checks": [ + "rules_leaked_password_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.7", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when Google detects compromised credentials requiring a reset of a user's password.", + "RationaleStatement": "Ensuring that administrators are alerted when Google detects that a user's credentials have been compromised due to a publicized breach. This is usually because the user has reused their credentials at another site that was breached.", + "ImpactStatement": "Emails will be sent to super administrators when triggered and in these cases, the user's password will need to be changed.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Leaked password and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to High 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the Leaked password shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Leaked password and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to Medium 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "Leaked password is ON", + "References": "https://support.google.com/a/answer/3230421" + } + ] + }, + { + "Id": "6.8", + "Description": "Ensure Gmail potential employee spoofing is configured", + "Checks": [ + "rules_gmail_employee_spoofing_alert_configured" + ], + "Attributes": [ + { + "Section": "6 Rules", + "SubSection": "6.8", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Configuring and enabling the setting that an alert will be generated when Google detects incoming messages are received where a sender’s name is in your Google Workspace directory, but the mail is not from your company’s domains or domain aliases.", + "RationaleStatement": "Ensuring that administrators are alerted when the email is being spoofed since this could be an indication of a phishing attempt.", + "ImpactStatement": "This setting should have no impact on the end user but will send emails to super administrators when triggered.", + "RemediationProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Gmail potential employee spoofing and select it. 5. Within the Actions pane, click the edit pencil on the right side of the pane. 6. Select Send to alert center (This will result in the alert being set to On). 7. Set the alert severity to Medium 8. To enable emails when this alert condition is met, select Send email notifications. Once enabled, the All super administrators option is selected by default. 9. Click Review to confirm the values. 10. Click Update Rule. 11. Confirm that the Gmail potential employee spoofing shows an Alert status of On in the list.", + "AuditProcedure": "To verify this setting via the Google Admin Console: 1. Log in to https://admin.google.com as an administrator. 2. Select Rules 3. Under Google protects you by default select View list. 4. Scroll to Gmail potential employee spoofing and select it. 5. Ensure that Alerts is set to On. 6. Ensure the Severity is set to Medium 7. Ensure that Email Notifications is set to On 8. Ensure that Email notification recipients is set to All super administrators", + "AdditionalInformation": "", + "DefaultValue": "Gmail potential employee spoofing is ON", + "References": "https://support.google.com/a/answer/3230421" + } + ] + } + ] +} diff --git a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json index 72ff97d97d..6bf49be05f 100644 --- a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json @@ -9,7 +9,6 @@ "Id": "GWS.COMMONCONTROLS.1.1", "Description": "Phishing-resistant MFA SHALL be required for all users", "Checks": [ - "security_2sv_enforced", "security_2sv_hardware_keys_admins" ], "Attributes": [ diff --git a/prowler/providers/googleworkspace/lib/service/service.py b/prowler/providers/googleworkspace/lib/service/service.py index 22454f3c63..35a10454cd 100644 --- a/prowler/providers/googleworkspace/lib/service/service.py +++ b/prowler/providers/googleworkspace/lib/service/service.py @@ -6,6 +6,11 @@ from prowler.providers.googleworkspace.googleworkspace_provider import ( GoogleworkspaceProvider, ) +# How far a Cloud Identity policy reaches. +CUSTOMER_SCOPE = "customer" +OVERRIDE_SCOPE = "override" +UNKNOWN_SCOPE = "unknown" + class GoogleWorkspaceService: def __init__( @@ -42,26 +47,29 @@ class GoogleWorkspaceService: ) return None - def _is_customer_level_policy(self, policy: dict) -> bool: - """Check if a policy applies at the customer (domain-wide) level. + def _policy_scope(self, policy: dict) -> str: + """Return how far a policy reaches: CUSTOMER_SCOPE, OVERRIDE_SCOPE or UNKNOWN_SCOPE. - The Cloud Identity Policy API typically scopes all policies to an OU; - absence of orgUnit is treated as customer-level as a safety net. - The root OU is equivalent to customer-level. This method accepts - policies with no orgUnit or policies targeting the root OU, - and rejects group-targeted and sub-OU policies. + The Cloud Identity Policy API typically scopes every policy to an OU, + and the root OU is equivalent to customer-level, so telling them apart + needs the root OU id. That id is fetched on a best-effort basis, and + without it a root-OU policy is indistinguishable from a sub-OU one: + that is UNKNOWN_SCOPE, which callers must not read as either. """ - policy_query = policy.get("policyQuery", {}) + policy_query = policy.get("policyQuery") or {} if policy_query.get("group"): - return False + return OVERRIDE_SCOPE org_unit = policy_query.get("orgUnit") if not org_unit: - return True - # Accept root OU as customer-level + return CUSTOMER_SCOPE root_id = getattr(self.provider.identity, "root_org_unit_id", None) - if root_id and org_unit == f"orgUnits/{root_id}": - return True - return False + if not root_id: + return UNKNOWN_SCOPE + return CUSTOMER_SCOPE if org_unit == f"orgUnits/{root_id}" else OVERRIDE_SCOPE + + def _is_customer_level_policy(self, policy: dict) -> bool: + """Whether a policy applies to the whole domain.""" + return self._policy_scope(policy) == CUSTOMER_SCOPE def _handle_api_error(self, error, context: str, resource_name: str = ""): """ diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json index 7e6be13d58..88035f633f 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when emails appear to come from domain names that look similar to the organization's domain. Lookalike domains are a common phishing technique used to trick users into trusting malicious messages.", + "Description": "Verifies that Gmail moves out of the inbox, to spam or quarantine, the emails that appear to come from domain names that look similar to the organization's domain. An action that only shows a warning is not enough. Lookalike domains are a common phishing technique used to trick users into trusting malicious messages.", "Risk": "Without protection against domain spoofing based on similar domain names, users may receive **phishing emails from lookalike domains** (e.g., examp1e.com instead of example.com) that appear legitimate. This enables **credential theft, malware delivery, and business email compromise** attacks.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py index d3a6bc94c1..5d674b60a0 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py @@ -2,6 +2,10 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client +from prowler.providers.googleworkspace.services.gmail.lib.spoofing import ( + describe_consequence, + is_protective, +) class gmail_domain_spoofing_protection_enabled(Check): @@ -9,7 +13,9 @@ class gmail_domain_spoofing_protection_enabled(Check): This check verifies that Gmail is configured to take action on emails that appear to come from similar-looking domain names, - helping prevent phishing via domain impersonation. + helping prevent phishing via domain impersonation. CIS requires the + configured action to move the message to spam or quarantine it, so an action + that only shows a warning is reported as a failure. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -26,38 +32,30 @@ class gmail_domain_spoofing_protection_enabled(Check): enabled = gmail_client.policies.detect_domain_name_spoofing consequence = gmail_client.policies.domain_spoofing_consequence + domain = gmail_client.provider.identity.domain if enabled is False: report.status = "FAIL" report.status_extended = ( - f"Protection against domain spoofing based on similar " - f"domain names is disabled in domain " - f"{gmail_client.provider.identity.domain}. " - f"Enable the protection and configure a protective action." + f"Protection against domain spoofing based on similar domain names " + f"is disabled in domain {domain}. " + f"Enable the protection and set the action to move the " + f"email to spam." ) - elif consequence == "NO_ACTION": + elif not is_protective(consequence): report.status = "FAIL" report.status_extended = ( - f"Protection against domain spoofing based on similar " - f"domain names is set to take no action in domain " - f"{gmail_client.provider.identity.domain}. " - f"A protective action should be configured." - ) - elif consequence is None: - report.status = "PASS" - report.status_extended = ( - f"Protection against domain spoofing based on similar " - f"domain names uses Google's secure default configuration " - f"(enabled) in domain " - f"{gmail_client.provider.identity.domain}." + f"Protection against domain spoofing based on similar domain names " + f"{describe_consequence(consequence)} in domain {domain}. " + f"The action should move the email to spam." ) else: report.status = "PASS" + state = "is enabled" if enabled else "uses Google's default (enabled)" report.status_extended = ( - f"Protection against domain spoofing based on similar " - f"domain names is enabled with consequence " - f"'{consequence}' in domain " - f"{gmail_client.provider.identity.domain}." + f"Protection against domain spoofing based on similar domain names " + f"{state} with action '{consequence}' in domain " + f"{domain}." ) findings.append(report) diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json index d6d107f360..103d09ea1e 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when the sender's display name matches an employee's name but the email comes from an external address. This is a common social engineering technique where attackers impersonate colleagues or executives.", + "Description": "Verifies that Gmail moves out of the inbox, to spam or quarantine, the emails whose sender display name matches an employee's name but come from an external address. An action that only shows a warning is not enough. This is a common social engineering technique where attackers impersonate colleagues or executives.", "Risk": "Without protection against employee name spoofing, users may receive **emails that appear to come from colleagues or executives** but are actually from external attackers. This enables **business email compromise (BEC)**, **wire fraud**, and **social engineering attacks** that exploit trust relationships.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py index ea6283c940..8f33bf4d6d 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py @@ -2,6 +2,10 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client +from prowler.providers.googleworkspace.services.gmail.lib.spoofing import ( + describe_consequence, + is_protective, +) class gmail_employee_name_spoofing_protection_enabled(Check): @@ -9,7 +13,9 @@ class gmail_employee_name_spoofing_protection_enabled(Check): This check verifies that Gmail is configured to take action on emails where the sender name matches an employee name but comes - from an external address, helping prevent social engineering attacks. + from an external address, helping prevent social engineering attacks. CIS requires the configured + action to move the message to spam or quarantine it, so an action that + only shows a warning is reported as a failure. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -26,36 +32,30 @@ class gmail_employee_name_spoofing_protection_enabled(Check): enabled = gmail_client.policies.detect_employee_name_spoofing consequence = gmail_client.policies.employee_name_spoofing_consequence + domain = gmail_client.provider.identity.domain if enabled is False: report.status = "FAIL" report.status_extended = ( - f"Protection against spoofing of employee names is " - f"disabled in domain " - f"{gmail_client.provider.identity.domain}. " - f"Enable the protection and configure a protective action." + f"Protection against spoofing of employee names " + f"is disabled in domain {domain}. " + f"Enable the protection and set the action to move the " + f"email to spam." ) - elif consequence == "NO_ACTION": + elif not is_protective(consequence): report.status = "FAIL" report.status_extended = ( - f"Protection against spoofing of employee names is set " - f"to take no action in domain " - f"{gmail_client.provider.identity.domain}. " - f"A protective action should be configured." - ) - elif consequence is None: - report.status = "PASS" - report.status_extended = ( - f"Protection against spoofing of employee names uses " - f"Google's secure default configuration (enabled) " - f"in domain {gmail_client.provider.identity.domain}." + f"Protection against spoofing of employee names " + f"{describe_consequence(consequence)} in domain {domain}. " + f"The action should move the email to spam." ) else: report.status = "PASS" + state = "is enabled" if enabled else "uses Google's default (enabled)" report.status_extended = ( - f"Protection against spoofing of employee names is " - f"enabled with consequence '{consequence}' in domain " - f"{gmail_client.provider.identity.domain}." + f"Protection against spoofing of employee names " + f"{state} with action '{consequence}' in domain " + f"{domain}." ) findings.append(report) diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json index c5f5ee61a9..0a8214370e 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when groups receive inbound emails that spoof the organization's domain. Google Groups are a high-value target because a single spoofed message can reach many recipients at once.", + "Description": "Verifies that Gmail moves out of the inbox, to spam or quarantine, the inbound emails to groups that spoof the organization's domain. An action that only shows a warning is not enough. Google Groups are a high-value target because a single spoofed message can reach many recipients at once.", "Risk": "Without protection of groups from domain-spoofing emails, attackers can send **spoofed messages to group mailboxes** that appear to originate from the organization. Since groups distribute to many recipients, a single spoofed email can enable **mass phishing, social engineering, or misinformation** campaigns across the organization.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py index fd4238239f..c4fb628e83 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py @@ -2,6 +2,10 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client +from prowler.providers.googleworkspace.services.gmail.lib.spoofing import ( + describe_consequence, + is_protective, +) class gmail_groups_spoofing_protection_enabled(Check): @@ -10,6 +14,9 @@ class gmail_groups_spoofing_protection_enabled(Check): This check verifies that Gmail is configured to take action on inbound emails to groups that spoof the organization's domain, helping prevent impersonation attacks targeting group mailboxes. + CIS requires the configured action to move the message to spam or + quarantine it, so an action that only shows a warning is reported as a + failure. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -27,56 +34,44 @@ class gmail_groups_spoofing_protection_enabled(Check): enabled = gmail_client.policies.detect_groups_spoofing consequence = gmail_client.policies.groups_spoofing_consequence visibility_type = gmail_client.policies.groups_spoofing_visibility_type + domain = gmail_client.provider.identity.domain + scope = ( + "private groups only" + if visibility_type == "PRIVATE_GROUPS_ONLY" + else "all groups" + ) if enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection of groups from inbound emails spoofing your " - f"domain is disabled in domain " - f"{gmail_client.provider.identity.domain}. " - f"Enable the protection and configure a protective action." + f"domain is disabled in domain {domain}. " + f"Enable the protection and set the action to move the " + f"email to spam." ) elif enabled is None: report.status = "FAIL" report.status_extended = ( f"Protection of groups from inbound emails spoofing your " f"domain is not configured and uses Google's insecure " - f"default (disabled) in domain " - f"{gmail_client.provider.identity.domain}. " - f"Enable the protection and configure a protective action." + f"default (disabled) in domain {domain}. " + f"Enable the protection and set the action to move the " + f"email to spam." ) - elif consequence == "NO_ACTION": + elif not is_protective(consequence): report.status = "FAIL" report.status_extended = ( f"Protection of groups from inbound emails spoofing your " - f"domain is set to take no action in domain " - f"{gmail_client.provider.identity.domain}. " - f"A protective action should be configured." - ) - elif consequence is None: - report.status = "PASS" - scope = ( - "private groups only" - if visibility_type == "PRIVATE_GROUPS_ONLY" - else "all groups" - ) - report.status_extended = ( - f"Protection of groups from inbound emails spoofing your " - f"domain is enabled for {scope} in domain " - f"{gmail_client.provider.identity.domain}." + f"domain is enabled for {scope} but " + f"{describe_consequence(consequence)} in domain {domain}. " + f"The action should move the email to spam." ) else: report.status = "PASS" - scope = ( - "private groups only" - if visibility_type == "PRIVATE_GROUPS_ONLY" - else "all groups" - ) report.status_extended = ( f"Protection of groups from inbound emails spoofing your " - f"domain is enabled for {scope} with consequence " - f"'{consequence}' in domain " - f"{gmail_client.provider.identity.domain}." + f"domain is enabled for {scope} with action " + f"'{consequence}' in domain {domain}." ) findings.append(report) diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json index a049a7ede5..fef0f77322 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json +++ b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when inbound emails spoof the organization's own domain. This protects against attackers sending emails that appear to originate from within the organization but are actually external.", + "Description": "Verifies that Gmail moves out of the inbox, to spam or quarantine, the inbound emails that spoof the organization's own domain. An action that only shows a warning is not enough. This protects against attackers sending emails that appear to originate from within the organization but are actually external.", "Risk": "Without protection against inbound domain spoofing, users may receive **emails that appear to come from their own organization** but are sent by external attackers. This enables **internal impersonation**, **phishing**, and **business email compromise** attacks that exploit trust in internal communications.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py index b9a22cadc6..822c445595 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py @@ -2,6 +2,10 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client +from prowler.providers.googleworkspace.services.gmail.lib.spoofing import ( + describe_consequence, + is_protective, +) class gmail_inbound_domain_spoofing_protection_enabled(Check): @@ -9,7 +13,9 @@ class gmail_inbound_domain_spoofing_protection_enabled(Check): This check verifies that Gmail is configured to take action on inbound emails that spoof the organization's own domain, helping - prevent impersonation of internal senders. + prevent impersonation of internal senders. CIS requires the configured + action to move the message to spam or quarantine it, so an action that + only shows a warning is reported as a failure. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -26,36 +32,30 @@ class gmail_inbound_domain_spoofing_protection_enabled(Check): enabled = gmail_client.policies.detect_inbound_domain_spoofing consequence = gmail_client.policies.inbound_domain_spoofing_consequence + domain = gmail_client.provider.identity.domain if enabled is False: report.status = "FAIL" report.status_extended = ( f"Protection against inbound emails spoofing your domain " - f"is disabled in domain " - f"{gmail_client.provider.identity.domain}. " - f"Enable the protection and configure a protective action." + f"is disabled in domain {domain}. " + f"Enable the protection and set the action to move the " + f"email to spam." ) - elif consequence == "NO_ACTION": + elif not is_protective(consequence): report.status = "FAIL" report.status_extended = ( f"Protection against inbound emails spoofing your domain " - f"is set to take no action in domain " - f"{gmail_client.provider.identity.domain}. " - f"A protective action should be configured." - ) - elif consequence is None: - report.status = "PASS" - report.status_extended = ( - f"Protection against inbound emails spoofing your domain " - f"uses Google's secure default configuration (enabled) " - f"in domain {gmail_client.provider.identity.domain}." + f"{describe_consequence(consequence)} in domain {domain}. " + f"The action should move the email to spam." ) else: report.status = "PASS" + state = "is enabled" if enabled else "uses Google's default (enabled)" report.status_extended = ( f"Protection against inbound emails spoofing your domain " - f"is enabled with consequence '{consequence}' " - f"in domain {gmail_client.provider.identity.domain}." + f"{state} with action '{consequence}' in domain " + f"{domain}." ) findings.append(report) diff --git a/prowler/providers/googleworkspace/services/gmail/lib/__init__.py b/prowler/providers/googleworkspace/services/gmail/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/lib/spoofing.py b/prowler/providers/googleworkspace/services/gmail/lib/spoofing.py new file mode 100644 index 0000000000..7c3b86a352 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/lib/spoofing.py @@ -0,0 +1,33 @@ +"""Helpers to evaluate the action configured for Gmail spoofing protections.""" + +from typing import Optional + +# Actions that actually keep the message away from the inbox. CIS Google +# Workspace 3.1.3.4.3.1, 3.1.3.4.3.2, 3.1.3.4.3.3 and 3.1.3.4.3.5 all require +# the action to be "Move email to spam"; quarantining is stricter and also +# satisfies the recommendation. +PROTECTIVE_CONSEQUENCES = {"SPAM_FOLDER", "QUARANTINE"} + +# Google leaves these protections enabled but set to "Keep email in inbox and +# show warning", which the benchmark does not accept, so an unset action is +# evaluated as the insecure default rather than as a secure one. +UNSET_CONSEQUENCE_DESCRIPTION = ( + "uses Google's default action (keep email in inbox and show a warning)" +) + +CONSEQUENCE_DESCRIPTIONS = { + "NO_ACTION": "is set to take no action", + "WARNING": "is set to keep the email in the inbox and show a warning", +} + + +def describe_consequence(consequence: Optional[str]) -> str: + """Return a human-readable description of a non-protective action.""" + if consequence is None: + return UNSET_CONSEQUENCE_DESCRIPTION + return CONSEQUENCE_DESCRIPTIONS.get(consequence, f"is set to '{consequence}'") + + +def is_protective(consequence: Optional[str]) -> bool: + """Whether the configured action moves the message out of the inbox.""" + return consequence in PROTECTIVE_CONSEQUENCES diff --git a/prowler/providers/googleworkspace/services/rules/lib/__init__.py b/prowler/providers/googleworkspace/services/rules/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/rules/lib/alerts.py b/prowler/providers/googleworkspace/services/rules/lib/alerts.py new file mode 100644 index 0000000000..c3c3754b67 --- /dev/null +++ b/prowler/providers/googleworkspace/services/rules/lib/alerts.py @@ -0,0 +1,127 @@ +"""Shared evaluation of the system-defined alert rules audited by CIS section 6.""" + +from typing import TYPE_CHECKING, List + +from prowler.lib.check.models import CheckReportGoogleWorkspace + +if TYPE_CHECKING: + from prowler.providers.googleworkspace.services.rules.rules_service import Rules + +# A rule classified above what the benchmark asks for is stricter, not weaker, +# so severities are compared by rank instead of by equality. +SEVERITY_RANK = {"LOW": 1, "MEDIUM": 2, "HIGH": 3} + +# The rule can be active while its delivery to the alert center is switched +# off, which is what the audit's "Ensure that Alerts is set to On" checks. +ALERT_CENTER_DISABLED = "DISABLED" + + +def _severity_issue(severity: str, minimum_severity: str) -> str: + """Return why a severity does not meet the benchmark, or an empty string.""" + if severity is None: + return f"severity is not configured (should be at least {minimum_severity})" + rank = SEVERITY_RANK.get(severity) + if rank is None: + return ( + f"severity is {severity}, which is not one of " + f"{', '.join(SEVERITY_RANK)}, so it could not be compared against " + f"the {minimum_severity} the benchmark asks for" + ) + if rank < SEVERITY_RANK[minimum_severity]: + return f"severity is {severity} (should be at least {minimum_severity})" + return "" + + +def evaluate_system_defined_alert( + client: "Rules", + metadata: dict, + rule_name: str, + minimum_severity: str, +) -> List[CheckReportGoogleWorkspace]: + """Report on one system-defined alert rule against the CIS audit procedure. + + Every recommendation in CIS section 6 asks for the rule to be on, to notify + by email, to include all super administrators as recipients and to carry a + minimum severity. Returns no finding at all when the policies could not be + fetched or the rule is not among the ones the client collected. + """ + findings = [] + + if not client.policies_fetched: + return findings + + for alert in client.system_defined_alerts: + if alert.display_name != rule_name: + continue + + domain = client.provider.identity.domain + report = CheckReportGoogleWorkspace( + metadata=metadata, + resource=alert, + resource_id=f"systemDefinedAlert/{rule_name}", + resource_name=rule_name, + customer_id=client.provider.identity.customer_id, + ) + + if alert.from_default: + # Nothing was observed: the state below is Google's documented + # default and the severity has no documented default at all. + if alert.state != "ACTIVE": + report.status = "FAIL" + report.status_extended = ( + f"System-defined alert rule '{rule_name}' was not returned " + f"by the API in domain {domain} and Google's default for it " + f"is OFF." + ) + else: + report.status = "MANUAL" + report.status_extended = ( + f"System-defined alert rule '{rule_name}' was not returned " + f"by the API in domain {domain}, so its configuration could " + f"not be verified. Review it in the Admin console: it should " + f"be ON, notify all super administrators by email and be set " + f"to {minimum_severity} severity or higher." + ) + findings.append(report) + continue + + issues = [] + + if alert.state != "ACTIVE": + issues.append("alert is OFF") + + # Only an explicit DISABLED fails. The API does not return this field + # even for a rule that is ON and has a severity set, and a severity + # cannot be configured for the alert center while delivery is off, so + # treating its absence as unverified would leave every one of these + # checks permanently MANUAL. + if alert.alert_center_status == ALERT_CENTER_DISABLED: + issues.append("the alert is not sent to the alert center") + + if not alert.email_notifications_enabled: + issues.append("email notifications are disabled") + elif not alert.all_super_admins: + issues.append("email recipients do not include all super administrators") + + severity = _severity_issue(alert.severity, minimum_severity) + if severity: + issues.append(severity) + + if issues: + report.status = "FAIL" + report.status_extended = ( + f"System-defined alert rule '{rule_name}' is not properly " + f"configured in domain {domain}: {', '.join(issues)}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"System-defined alert rule '{rule_name}' is properly " + f"configured in domain {domain}: alert is ON, email " + f"notifications are enabled, recipients include all super " + f"administrators and severity is {alert.severity}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.metadata.json index a93b4fb737..84a665b8c6 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **User granted Admin privilege** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when a user is given elevated admin privileges.", + "Description": "The **User granted Admin privilege** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to Medium or higher. This ensures administrators are notified when a user is given elevated admin privileges.", "Risk": "Without this alert enabled, administrators will not be notified when users receive **elevated admin privileges**. Unauthorized privilege escalation could indicate account compromise or insider threats and requires immediate verification.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.py index 55b9a685bf..30ebec64e4 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "User granted Admin privilege" +MINIMUM_SEVERITY = "MEDIUM" class rules_admin_privilege_granted_alert_configured(Check): - """Check that the User granted Admin privilege system-defined alert rule is fully configured.""" + """Check that the User granted Admin privilege system-defined alert rule is fully configured. + + CIS 6.4 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to MEDIUM severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.metadata.json index 34885cb605..1966208719 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **Gmail potential employee spoofing** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when incoming messages have a sender name matching the directory but from an external domain.", + "Description": "The **Gmail potential employee spoofing** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to Medium or higher. This ensures administrators are notified when incoming messages have a sender name matching the directory but from an external domain.", "Risk": "Without this alert enabled, administrators will not be notified of potential **employee spoofing via email**. Attackers may impersonate internal employees using external email addresses to conduct phishing attacks against the organization.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.py index 0993f72d3d..4c648a43d6 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "Gmail potential employee spoofing" +MINIMUM_SEVERITY = "MEDIUM" class rules_gmail_employee_spoofing_alert_configured(Check): - """Check that the Gmail potential employee spoofing system-defined alert rule is fully configured.""" + """Check that the Gmail potential employee spoofing system-defined alert rule is fully configured. + + CIS 6.8 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to MEDIUM severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.metadata.json index 35fa25f248..3769f7b132 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **Government-backed attacks** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google believes users are being targeted by a government-backed attacker.", + "Description": "The **Government-backed attacks** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to High or higher. This ensures administrators are notified when Google believes users are being targeted by a government-backed attacker.", "Risk": "Without this alert enabled, administrators will not be notified of potential **government-backed attacks** targeting their users. These attacks are sophisticated and require immediate response to protect affected accounts and investigate the threat.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.py index b566d99e0c..aa8eec5ca0 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "Government-backed attacks" +MINIMUM_SEVERITY = "HIGH" class rules_government_backed_attacks_alert_configured(Check): - """Check that the Government-backed attacks system-defined alert rule is fully configured.""" + """Check that the Government-backed attacks system-defined alert rule is fully configured. + + CIS 6.2 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to HIGH severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.metadata.json index 870144a873..8866d76e02 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **Leaked password** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google detects compromised credentials requiring a password reset.", + "Description": "The **Leaked password** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to Medium or higher. This ensures administrators are notified when Google detects compromised credentials requiring a password reset.", "Risk": "Without this alert enabled, administrators will not be notified when Google detects that a user's **credentials have been compromised** in a publicized breach. The user likely reused their password at another site that was breached, and their account requires an immediate password change.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.py index 797bed4f71..8134661eeb 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "Leaked password" +MINIMUM_SEVERITY = "MEDIUM" class rules_leaked_password_alert_configured(Check): - """Check that the Leaked password system-defined alert rule is fully configured.""" + """Check that the Leaked password system-defined alert rule is fully configured. + + CIS 6.7 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to MEDIUM severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.metadata.json index f1f4fbc622..bdd6a84d5f 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **User's password changed** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are promptly notified when user passwords are changed.", + "Description": "The **User's password changed** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to Medium or higher. This ensures administrators are promptly notified when user passwords are changed.", "Risk": "Without this alert enabled, administrators will not be notified when user passwords are changed. This could allow **credential compromise and account takeover** to go undetected, giving attackers time to establish persistence.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.py index fb6382caf8..c61c6e9d76 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "User's password changed" +MINIMUM_SEVERITY = "MEDIUM" class rules_password_changed_alert_configured(Check): - """Check that the User's password changed system-defined alert rule is fully configured.""" + """Check that the User's password changed system-defined alert rule is fully configured. + + CIS 6.1 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to MEDIUM severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/rules/rules_service.py b/prowler/providers/googleworkspace/services/rules/rules_service.py index 76b0b0df7a..962548d7b4 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_service.py +++ b/prowler/providers/googleworkspace/services/rules/rules_service.py @@ -90,6 +90,7 @@ class Rules(GoogleWorkspaceService): state=default_state, email_notifications_enabled=is_active_default, all_super_admins=is_active_default, + from_default=True, ) logger.debug( f"System-defined alert rule (default): {rule_name} " @@ -119,7 +120,12 @@ class Rules(GoogleWorkspaceService): state = value.get("state", "INACTIVE") alert_center_action = value.get("action", {}).get("alertCenterAction", {}) - severity = alert_center_action.get("alertCenterConfig", {}).get("severity") + alert_center_config = alert_center_action.get("alertCenterConfig", {}) + severity = alert_center_config.get("severity") + # CIS remediation step 6: "Select Send to alert center (This will result + # in the alert being set to On)", so this is the toggle the audit's + # "Ensure that Alerts is set to On" refers to. + alert_center_status = alert_center_config.get("status") recipients = alert_center_action.get("recipients", []) all_super_admins = any(r.get("allSuperAdmins") is True for r in recipients) @@ -128,6 +134,7 @@ class Rules(GoogleWorkspaceService): display_name=display_name, state=state, severity=severity, + alert_center_status=alert_center_status, email_notifications_enabled=len(recipients) > 0, all_super_admins=all_super_admins, ) @@ -139,5 +146,10 @@ class SystemDefinedAlert(BaseModel): display_name: str state: str = "INACTIVE" severity: Optional[str] = None + # rule.system_defined_alerts action.alertCenterAction.alertCenterConfig.status + alert_center_status: Optional[str] = None email_notifications_enabled: bool = False all_super_admins: bool = False + # True when the API returned no policy for the rule and the values above + # were inferred from Google's documented defaults rather than observed. + from_default: bool = False diff --git a/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.metadata.json index b79120abde..af11bb2e0d 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **User suspended due to suspicious activity** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google suspends an account due to a potential compromise.", + "Description": "The **User suspended due to suspicious activity** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to High or higher. This ensures administrators are notified when Google suspends an account due to a potential compromise.", "Risk": "Without this alert enabled, administrators will not be promptly notified when Google **suspends a user account** due to detected compromise. The suspended user cannot work, and the underlying security incident requires immediate investigation.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.py index cd243be8e3..f9f4c2cabe 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "User suspended due to suspicious activity" +MINIMUM_SEVERITY = "HIGH" class rules_suspicious_activity_suspension_alert_configured(Check): - """Check that the User suspended due to suspicious activity system-defined alert rule is fully configured.""" + """Check that the User suspended due to suspicious activity system-defined alert rule is fully configured. + + CIS 6.3 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to HIGH severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.metadata.json index 93af99b565..132a3fd3e2 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "low", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **Suspicious login** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google detects a sign-in attempt that does not match a user's normal behavior.", + "Description": "The **Suspicious login** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to Low or higher. This ensures administrators are notified when Google detects a sign-in attempt that does not match a user's normal behavior.", "Risk": "Without this alert enabled, administrators will not be notified of **suspicious login attempts** such as sign-ins from unusual locations. This could indicate an active attack using previously obtained credentials.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.py index eee7844c43..951015c0e8 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "Suspicious login" +MINIMUM_SEVERITY = "LOW" class rules_suspicious_login_alert_configured(Check): - """Check that the Suspicious login system-defined alert rule is fully configured.""" + """Check that the Suspicious login system-defined alert rule is fully configured. + + CIS 6.6 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to LOW severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.metadata.json b/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.metadata.json index 202df2adad..e4979b65d3 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.metadata.json +++ b/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.metadata.json @@ -9,7 +9,7 @@ "Severity": "low", "ResourceType": "NotDefined", "ResourceGroup": "monitoring", - "Description": "The **Suspicious programmatic login** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google detects suspicious login attempts from applications or programs.", + "Description": "The **Suspicious programmatic login** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, recipients set to all super administrators, and severity set to Low or higher. This ensures administrators are notified when Google detects suspicious login attempts from applications or programs.", "Risk": "Without this alert enabled, administrators will not be notified of **suspicious programmatic login attempts**. This could indicate automated credential stuffing or unauthorized API access using compromised credentials.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.py b/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.py index 0d609a99e1..377da9e1c9 100644 --- a/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.py +++ b/prowler/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured.py @@ -1,61 +1,25 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) from prowler.providers.googleworkspace.services.rules.rules_client import ( rules_client, ) RULE_NAME = "Suspicious programmatic login" +MINIMUM_SEVERITY = "LOW" class rules_suspicious_programmatic_login_alert_configured(Check): - """Check that the Suspicious programmatic login system-defined alert rule is fully configured.""" + """Check that the Suspicious programmatic login system-defined alert rule is fully configured. + + CIS 6.5 requires the rule to be on, to notify by email, to include all + super administrators as recipients and to be set to LOW severity or higher. + """ def execute(self) -> List[CheckReportGoogleWorkspace]: - findings = [] - - if rules_client.policies_fetched: - for alert in rules_client.system_defined_alerts: - if alert.display_name != RULE_NAME: - continue - - domain = rules_client.provider.identity.domain - report = CheckReportGoogleWorkspace( - metadata=self.metadata(), - resource=alert, - resource_id=f"systemDefinedAlert/{RULE_NAME}", - resource_name=RULE_NAME, - customer_id=rules_client.provider.identity.customer_id, - ) - - is_active = alert.state == "ACTIVE" - has_recipients = alert.email_notifications_enabled - all_super_admins = alert.all_super_admins - - if is_active and has_recipients and all_super_admins: - report.status = "PASS" - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is properly " - f"configured in domain {domain}: alert is ON, email " - f"notifications are enabled, and recipients include " - f"all super administrators." - ) - else: - report.status = "FAIL" - issues = [] - if not is_active: - issues.append("alert is OFF") - if not has_recipients: - issues.append("email notifications are disabled") - elif not all_super_admins: - issues.append( - "email recipients do not include all super administrators" - ) - report.status_extended = ( - f"System-defined alert rule '{RULE_NAME}' is not properly " - f"configured in domain {domain}: {', '.join(issues)}." - ) - - findings.append(report) - - return findings + return evaluate_system_defined_alert( + rules_client, self.metadata(), RULE_NAME, MINIMUM_SEVERITY + ) diff --git a/prowler/providers/googleworkspace/services/security/lib/__init__.py b/prowler/providers/googleworkspace/services/security/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/security/lib/durations.py b/prowler/providers/googleworkspace/services/security/lib/durations.py new file mode 100644 index 0000000000..93736e9b7d --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/lib/durations.py @@ -0,0 +1,89 @@ +"""Helpers for the duration and timestamp values of the Cloud Identity security policies.""" + +import re +from datetime import datetime, timezone +from typing import Optional + +from dateutil import parser as date_parser + +_DURATION = re.compile(r"^(\d+(?:\.\d+)?)s$") + +ONE_HOUR_SECONDS = 3600 +ONE_DAY_SECONDS = 86400 +TWO_WEEKS_SECONDS = 1209600 +ONE_YEAR_SECONDS = 31536000 + +# The API reports enforcement being OFF as the protobuf zero-value Timestamp +# rather than as a null or an empty string. +_ENFORCEMENT_OFF_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) + + +def parse_duration_seconds(value: Optional[str]) -> Optional[int]: + """Return the seconds in a protobuf duration string such as "1209600s". + + Returns None when the value is missing or not a duration Prowler knows how + to read, so callers can tell "not configured" apart from a real length. + """ + if not value or not isinstance(value, str): + return None + match = _DURATION.match(value.strip()) + if not match: + return None + return int(float(match.group(1))) + + +def format_duration(value: Optional[str]) -> str: + """Render a duration string in the largest whole unit, for a finding message.""" + seconds = parse_duration_seconds(value) + if seconds is None: + return "not configured" + if seconds == 0: + return "none" + for unit_seconds, name in ( + (ONE_DAY_SECONDS, "day"), + (ONE_HOUR_SECONDS, "hour"), + ): + units = seconds / unit_seconds + if units.is_integer(): + return f"{int(units)} {name}(s)" + return f"{seconds} second(s)" + + +def _parse_timestamp(value: Optional[str]) -> Optional[datetime]: + """Parse an API timestamp, tolerating any fractional-second precision. + + protobuf emits up to nanosecond precision, which `datetime.fromisoformat` + rejects before Python 3.11, so the shared dateutil parser is used instead. + """ + if not value or not isinstance(value, str): + return None + try: + parsed = date_parser.isoparse(value) + except (ValueError, OverflowError): + return None + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return parsed + + +def enforcement_issue( + enforced_from: Optional[str], + allow_scheduled: bool = False, + now: Optional[datetime] = None, +) -> Optional[str]: + """Return why 2-Step Verification enforcement is not in effect, or None. + + Google accepts a future start date, which means the policy is scheduled but + not yet applied to anyone. CIS 4.1.1.2 accepts "On from " explicitly + while 4.1.1.1 and 4.1.1.3 ask for plain "On", hence `allow_scheduled`. + """ + if not enforced_from: + return "enforcement is not configured and defaults to OFF" + parsed = _parse_timestamp(enforced_from) + if parsed is None: + return f"the enforcement start date '{enforced_from}' could not be read" + if parsed <= _ENFORCEMENT_OFF_EPOCH: + return "enforcement is set to OFF" + if not allow_scheduled and parsed > (now or datetime.now(timezone.utc)): + return f"enforcement does not start until {enforced_from}" + return None diff --git a/prowler/providers/googleworkspace/services/security/lib/scope.py b/prowler/providers/googleworkspace/services/security/lib/scope.py new file mode 100644 index 0000000000..0ef08d2808 --- /dev/null +++ b/prowler/providers/googleworkspace/services/security/lib/scope.py @@ -0,0 +1,46 @@ +"""Whether the domain-wide policy values describe what every user actually gets.""" + +from typing import FrozenSet, List, Optional + + +def _listing(settings: List[str]) -> str: + return f"{', '.join(settings)} {'are' if len(settings) > 1 else 'is'}" + + +def unevaluable_reason(policies, evaluated_settings: FrozenSet[str]) -> Optional[str]: + """Return why the domain-wide values cannot be judged at all, or None. + + In both cases the values a check would read were never reported, so every + condition it evaluates would be built on Prowler's own defaults. + """ + if policies.unresolved_scope: + return ( + "the root organizational unit could not be resolved, so the " + "domain-wide policies could not be told apart from the ones scoped " + "to an organizational unit" + ) + unobserved = sorted(set(policies.unobserved_settings) & evaluated_settings) + if unobserved: + return ( + f"{_listing(unobserved)} only configured for a group or an " + f"organizational unit, so no domain-wide value was reported" + ) + return None + + +def failures_shadowed_by_overrides(policies, failing_settings: FrozenSet[str]) -> bool: + """Whether every failing setting is also overridden below the domain.""" + return bool(failing_settings) and failing_settings <= set( + policies.overridden_settings + ) + + +def override_caveat(policies, evaluated_settings: FrozenSet[str]) -> str: + """Return what a group or an OU also overrides on top of the domain, or an empty string.""" + overridden = sorted(set(policies.overridden_settings) & evaluated_settings) + if not overridden: + return "" + return ( + f"{_listing(overridden)} also overridden for at least one group or " + f"organizational unit, so this does not describe every user" + ) 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 index 3a2c0b4566..c9e790a7b8 100644 --- 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 @@ -9,7 +9,7 @@ "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.", + "Description": "The domain-level policy **enforces 2-Step Verification** for all users, allows users to turn it on, keeps the new user enrollment period at two weeks or less, disables device trust and excludes verification codes via text or phone call from the accepted methods.", "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": [ @@ -35,5 +35,5 @@ "RelatedTo": [ "security_2sv_hardware_keys_admins" ], - "Notes": "" + "Notes": "CIS 4.1.1.1 audits the group holding every admin role, but the Cloud Identity Policy API returns domain-wide policies only. This check evaluates the customer-level policy, which applies to administrators as well, so it cannot confirm a separate admin-role group is configured." } 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 index 7cf17b348e..e5bf69c5ca 100644 --- 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 @@ -1,17 +1,54 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.lib.durations import ( + TWO_WEEKS_SECONDS, + enforcement_issue, + format_duration, + parse_duration_seconds, +) +from prowler.providers.googleworkspace.services.security.lib.scope import ( + failures_shadowed_by_overrides, + override_caveat, + unevaluable_reason, +) from prowler.providers.googleworkspace.services.security.security_client import ( security_client, ) +# "Methods: Any except verification codes via text, phone call". Listed as an +# allow list so a value Prowler does not know cannot pass by not being "ALL". +TELEPHONY_FREE_FACTOR_SETS = { + "NO_TELEPHONY", + "PASSKEY_ONLY", + "PASSKEY_PLUS_SECURITY_CODE", + "PASSKEY_PLUS_IP_BOUND_SECURITY_CODE", +} + +# The settings this check reads, to tell whether an override reaches it. +EVALUATED_SETTINGS = frozenset( + { + "security.two_step_verification_enrollment", + "security.two_step_verification_enforcement", + "security.two_step_verification_enforcement_factor", + "security.two_step_verification_device_trust", + "security.two_step_verification_grace_period", + } +) + class security_2sv_enforced(Check): - """Check that 2-Step Verification is enforced for all users. + """Check that 2-Step Verification is enforced following the CIS audit steps. - 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. + CIS 4.1.1.1 and 4.1.1.3 ask for more than enforcement being on: users must + be allowed to turn 2-Step Verification on, the new user enrollment period + must not exceed two weeks, device trust must be off and verification codes + via text or phone call must not be an accepted method. Each of those is + evaluated here so the requirement cannot pass on enforcement alone. + + Note: 4.1.1.1 audits the group holding every admin role, but the Cloud + Identity Policy API returns domain-wide policies only. This check evaluates + the customer-level policy, which applies to administrators too. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -26,33 +63,125 @@ class security_2sv_enforced(Check): 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" + policies = security_client.policies + domain = security_client.provider.identity.domain - if enforced_from and enforced_from != enforcement_off_epoch: - report.status = "PASS" + unevaluable = unevaluable_reason(policies, EVALUATED_SETTINGS) + if unevaluable: + report.status = "MANUAL" report.status_extended = ( - f"2-Step Verification enforcement is active " - f"(enforced from {enforced_from}) " - f"in domain {security_client.provider.identity.domain}." + f"2-Step Verification could not be evaluated in domain " + f"{domain}: {unevaluable}. Review it in the Admin console." + ) + findings.append(report) + return findings + + caveat = override_caveat(policies, EVALUATED_SETTINGS) + issues = [] # (setting, why it fails) + + enforced_from = policies.two_sv_enforced_from + enforcement = enforcement_issue(enforced_from) + if enforcement: + issues.append( + ("security.two_step_verification_enforcement", enforcement) + ) + + if policies.two_sv_allow_enrollment is False: + issues.append( + ( + "security.two_step_verification_enrollment", + "users are not allowed to turn on 2-Step Verification", + ) + ) + + # Google's default is no enrollment period, which is stricter than + # the two weeks the benchmark asks for, so only longer periods fail. + # A value Prowler cannot read fails closed rather than being skipped. + raw_grace_period = policies.two_sv_enrollment_grace_period + grace_period = parse_duration_seconds(raw_grace_period) + if raw_grace_period and grace_period is None: + issues.append( + ( + "security.two_step_verification_grace_period", + f"the new user enrollment period '{raw_grace_period}' " + f"could not be read", + ) + ) + elif grace_period is not None and grace_period > TWO_WEEKS_SECONDS: + issues.append( + ( + "security.two_step_verification_grace_period", + f"the new user enrollment period is " + f"{format_duration(policies.two_sv_enrollment_grace_period)} " + f"(should not exceed 2 weeks)", + ) + ) + + if policies.two_sv_allow_trusting_device is not False: + issues.append( + ( + "security.two_step_verification_device_trust", + ( + "users are allowed to trust their device" + if policies.two_sv_allow_trusting_device + else "device trust is not configured and defaults to allowed" + ), + ) + ) + + factor_set = policies.two_sv_allowed_factor_set + if factor_set not in TELEPHONY_FREE_FACTOR_SETS: + issues.append( + ( + "security.two_step_verification_enforcement_factor", + ( + "the allowed methods are not configured and default to " + "any method, including verification codes via text and " + "phone call" + if factor_set is None + else f"the allowed methods are {factor_set}, which does " + f"not exclude verification codes via text and phone call" + ), + ) + ) + + failing_settings = frozenset(setting for setting, _ in issues) + reasons = "; ".join(text for _, text in issues) + + if issues and failures_shadowed_by_overrides(policies, failing_settings): + # The audited scope (e.g. the admin group of 4.1.1.1) may get + # the overriding value, which the Policy API does not expose, + # so the domain-wide failure cannot be confirmed for it. + report.status = "MANUAL" + report.status_extended = ( + f"2-Step Verification is not enforced as required in the " + f"domain-wide policy of {domain}: {reasons}. However, every " + f"failing setting is also overridden for at least one group " + f"or organizational unit, so the audited scope may be " + f"configured correctly. Review those overrides in the Admin " + f"console." + ) + elif issues: + report.status = "FAIL" + report.status_extended = ( + f"2-Step Verification is not enforced as required in domain " + f"{domain}: {reasons}." + (f" Note: {caveat}." if caveat else "") + ) + elif caveat: + report.status = "MANUAL" + report.status_extended = ( + f"2-Step Verification meets the benchmark in the domain-wide " + f"policy of {domain}, but {caveat}. Review those overrides " + f"in the Admin console." ) 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." - ) + report.status = "PASS" + report.status_extended = ( + f"2-Step Verification is enforced in domain {domain} " + f"(enforced from {enforced_from}), device trust is disabled " + f"and verification codes via text or phone call are not an " + f"accepted method." + ) findings.append(report) 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 index 4aae5840a5..cae39072dc 100644 --- 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 @@ -9,7 +9,7 @@ "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.", + "Description": "The domain-level 2-Step Verification policy requires **hardware security keys only** as the allowed sign-in factor, with enforcement on or scheduled and a policy suspension grace period of at most one day. **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": [ 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 index 5913f448e1..c95c67a72f 100644 --- 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 @@ -1,20 +1,50 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.lib.durations import ( + ONE_DAY_SECONDS, + enforcement_issue, + format_duration, + parse_duration_seconds, +) +from prowler.providers.googleworkspace.services.security.lib.scope import ( + failures_shadowed_by_overrides, + override_caveat, + unevaluable_reason, +) from prowler.providers.googleworkspace.services.security.security_client import ( security_client, ) +# "Methods: Only security key". The values that also accept security codes are +# PASSKEY_PLUS_SECURITY_CODE and PASSKEY_PLUS_IP_BOUND_SECURITY_CODE, so +# requiring this one covers the benchmark's "don't allow users to generate +# security codes" step as well. +SECURITY_KEYS_ONLY = "PASSKEY_ONLY" + +# The settings this check reads, to tell whether an override reaches it. +EVALUATED_SETTINGS = frozenset( + { + "security.two_step_verification_enrollment", + "security.two_step_verification_enforcement", + "security.two_step_verification_enforcement_factor", + "security.two_step_verification_sign_in_code", + } +) + 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. + CIS 4.1.1.2 asks for security keys to be the only accepted method, with + enrollment allowed, enforcement on or scheduled, and a policy suspension + grace period of at most one day, so the requirement cannot pass on the + accepted method alone. + + 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, + so a passing domain-wide policy also covers the administrative accounts. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -29,35 +59,119 @@ class security_2sv_hardware_keys_admins(Check): customer_id=security_client.provider.identity.customer_id, ) - factor_set = security_client.policies.two_sv_allowed_factor_set + policies = security_client.policies + domain = security_client.provider.identity.domain - if factor_set == "PASSKEY_ONLY": - report.status = "PASS" + unevaluable = unevaluable_reason(policies, EVALUATED_SETTINGS) + if unevaluable: + report.status = "MANUAL" report.status_extended = ( - f"2-Step Verification enforcement requires security keys only " - f"in domain {security_client.provider.identity.domain}." + f"2-Step Verification could not be evaluated in domain " + f"{domain}: {unevaluable}. Review it in the Admin console." + ) + findings.append(report) + return findings + + caveat = override_caveat(policies, EVALUATED_SETTINGS) + issues = [] # (setting, why it fails) + + factor_set = policies.two_sv_allowed_factor_set + if factor_set != SECURITY_KEYS_ONLY: + issues.append( + ( + "security.two_step_verification_enforcement_factor", + ( + "the accepted method is not configured and defaults to " + "any method, including SMS and phone call" + if factor_set is None + else f"the accepted method is {factor_set} " + f"(should be {SECURITY_KEYS_ONLY})" + ), + ) + ) + + # 4.1.1.2 accepts "On from ", unlike 4.1.1.1 and 4.1.1.3. + enforcement = enforcement_issue( + policies.two_sv_enforced_from, allow_scheduled=True + ) + if enforcement: + issues.append( + ("security.two_step_verification_enforcement", enforcement) + ) + + if policies.two_sv_allow_enrollment is False: + issues.append( + ( + "security.two_step_verification_enrollment", + "users are not allowed to turn on 2-Step Verification", + ) + ) + + # Google's default is no suspension grace period, which is stricter + # than the one day the benchmark asks for, so only longer periods + # fail. A value Prowler cannot read fails closed. + raw_grace_period = policies.two_sv_backup_code_exception_period + grace_period = parse_duration_seconds(raw_grace_period) + if raw_grace_period and grace_period is None: + issues.append( + ( + "security.two_step_verification_sign_in_code", + f"the 2-Step Verification policy suspension grace period " + f"'{raw_grace_period}' could not be read", + ) + ) + elif grace_period is not None and grace_period > ONE_DAY_SECONDS: + issues.append( + ( + "security.two_step_verification_sign_in_code", + f"the 2-Step Verification policy suspension grace period " + f"is {format_duration(raw_grace_period)} " + f"(should not exceed 1 day)", + ) + ) + + failing_settings = frozenset(setting for setting, _ in issues) + reasons = "; ".join(text for _, text in issues) + + if issues and failures_shadowed_by_overrides(policies, failing_settings): + # The admin group of 4.1.1.2 may get the overriding value, + # which the Policy API does not expose, so the domain-wide + # failure cannot be confirmed for it. + report.status = "MANUAL" + report.status_extended = ( + f"2-Step Verification does not require security keys in the " + f"domain-wide policy of {domain}: {reasons}. However, every " + f"failing setting is also overridden for at least one group " + f"or organizational unit, so the administrative accounts may " + f"be configured correctly. Review those overrides in the " + f"Admin console." + ) + elif issues: + report.status = "FAIL" + report.status_extended = ( + f"2-Step Verification does not require security keys as " + f"configured in domain {domain}: {reasons}. " + + (f"Note: {caveat}. " if caveat else "") + + "Note: this check evaluates the domain-wide policy, the " + "Policy API does not expose role-specific 2SV enforcement." + ) + elif caveat: + report.status = "MANUAL" + report.status_extended = ( + f"2-Step Verification meets the benchmark in the domain-wide " + f"policy of {domain}, but {caveat}. Review those overrides " + f"in the Admin console." ) 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." - ) + report.status = "PASS" + report.status_extended = ( + f"2-Step Verification requires security keys only in domain " + f"{domain}, enforcement is on or scheduled and the policy " + f"suspension grace period is " + f"{format_duration(policies.two_sv_backup_code_exception_period)}. " + f"Note: this check evaluates the domain-wide policy, the " + f"Policy API does not expose role-specific 2SV enforcement." + ) findings.append(report) 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 index ec945474fc..c02ab1a669 100644 --- 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 @@ -9,7 +9,7 @@ "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.", + "Description": "The domain-level login challenges configuration has the **employee ID challenge disabled**. This check is **not mapped to CIS 4.1.4.1** because that recommendation also requires Post-SSO verification, a setting **not exposed by the Cloud Identity Policy API**, so the requirement cannot be evaluated end to end.", "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": [ 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 index e3732053b8..68de612b65 100644 --- 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 @@ -9,11 +9,11 @@ from prowler.providers.googleworkspace.services.security.security_client import 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. + This check verifies that the employee ID login challenge is disabled. + + It is not mapped to CIS 4.1.4.1: that recommendation also requires Post-SSO + verification, a setting the Cloud Identity Policy API does not expose, so + the requirement cannot be evaluated end to end and is reported as manual. """ def execute(self) -> List[CheckReportGoogleWorkspace]: 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 index 33448aa536..82118537e0 100644 --- 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 @@ -1,6 +1,11 @@ from typing import List from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.security.lib.durations import ( + ONE_YEAR_SECONDS, + format_duration, + parse_duration_seconds, +) from prowler.providers.googleworkspace.services.security.security_client import ( security_client, ) @@ -11,8 +16,9 @@ class security_password_policy_strong(Check): 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. + password reuse disallowed, enforcement at next sign-in, and a password + reset frequency of 365 days or less. Shorter periods are more restrictive + than the benchmark asks for, so only longer ones fail. """ def execute(self) -> List[CheckReportGoogleWorkspace]: @@ -39,12 +45,11 @@ class security_password_policy_strong(Check): 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)" - ) + # Google enforces strong passwords by default, so an unset value is + # the secure default rather than a missing configuration. + strength = policies.password_allowed_strength + if strength is not None and strength != "STRONG": + issues.append(f"password strength is {strength} (requires STRONG)") if policies.password_allow_reuse is True: issues.append("password reuse is allowed") @@ -52,9 +57,22 @@ class security_password_policy_strong(Check): 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": + raw_expiration = policies.password_expiration_duration + expiration = parse_duration_seconds(raw_expiration) + if raw_expiration and expiration is None: + issues.append( + f"password expiration '{raw_expiration}' could not be read" + ) + elif expiration is None: issues.append("password expiration is not configured") + elif expiration == 0: + issues.append("passwords are set to never expire") + elif expiration > ONE_YEAR_SECONDS: + issues.append( + f"password expiration is " + f"{format_duration(policies.password_expiration_duration)} " + f"(requires 365 days or less)" + ) if not issues: report.status = "PASS" @@ -62,7 +80,8 @@ class security_password_policy_strong(Check): 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." + f"enforced at next sign-in, expiration " + f"{format_duration(policies.password_expiration_duration)}." ) else: report.status = "FAIL" diff --git a/prowler/providers/googleworkspace/services/security/security_service.py b/prowler/providers/googleworkspace/services/security/security_service.py index 96f24061f8..dd0cbff557 100644 --- a/prowler/providers/googleworkspace/services/security/security_service.py +++ b/prowler/providers/googleworkspace/services/security/security_service.py @@ -1,9 +1,13 @@ -from typing import Optional +from typing import List, Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field from prowler.lib.logger import logger -from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService +from prowler.providers.googleworkspace.lib.service.service import ( + CUSTOMER_SCOPE, + UNKNOWN_SCOPE, + GoogleWorkspaceService, +) class Security(GoogleWorkspaceService): @@ -18,6 +22,8 @@ class Security(GoogleWorkspaceService): super().__init__(provider) self.policies = SecurityPolicies() self.policies_fetched = False + self._overridden = set() + self._observed = set() self._fetch_security_policies() def _fetch_security_policies(self): @@ -48,6 +54,10 @@ class Security(GoogleWorkspaceService): service, 'setting.type.matches("rule.dlp")', fetch_succeeded ) + self.policies.overridden_settings = sorted(self._overridden) + self.policies.unobserved_settings = sorted( + self._overridden - self._observed + ) self.policies_fetched = fetch_succeeded if fetch_succeeded: @@ -79,11 +89,18 @@ class Security(GoogleWorkspaceService): 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/") + + scope = self._policy_scope(policy) + if scope != CUSTOMER_SCOPE: + if scope == UNKNOWN_SCOPE: + self.policies.unresolved_scope = True + elif setting_type: + self._overridden.add(setting_type) + continue + + self._observed.add(setting_type) value = setting.get("value", {}) self._process_setting(setting_type, value) @@ -239,6 +256,17 @@ class Security(GoogleWorkspaceService): class SecurityPolicies(BaseModel): """Model for domain-level Security policy settings.""" + # Setting types that a group or a sub-OU overrides. Sorted lists rather than + # sets: a set reaches the OCSF output as its Python repr, in a different + # order on every scan. + overridden_settings: List[str] = Field(default_factory=list) + # Overridden settings with no domain-wide policy of their own, so the values + # below are Prowler's defaults and not something the domain reported. + unobserved_settings: List[str] = Field(default_factory=list) + # True when a policy's scope could not be determined because the root + # organizational unit id is unknown, which blanks the values below. + unresolved_scope: bool = False + # security.two_step_verification_enrollment two_sv_allow_enrollment: Optional[bool] = None # security.two_step_verification_enforcement diff --git a/tests/providers/googleworkspace/googleworkspace_fixtures.py b/tests/providers/googleworkspace/googleworkspace_fixtures.py index 72744b6244..96e50ce2e0 100644 --- a/tests/providers/googleworkspace/googleworkspace_fixtures.py +++ b/tests/providers/googleworkspace/googleworkspace_fixtures.py @@ -1,5 +1,6 @@ """Test fixtures for Google Workspace provider tests""" +from typing import Optional from unittest.mock import MagicMock from prowler.providers.googleworkspace.models import ( @@ -81,17 +82,19 @@ ROLE_GROUPS_ADMIN = { def set_mocked_googleworkspace_provider( - identity: GoogleWorkspaceIdentityInfo = GoogleWorkspaceIdentityInfo( + identity: Optional[GoogleWorkspaceIdentityInfo] = None, +): + provider = MagicMock() + provider.type = "googleworkspace" + # Built per call: as a default argument every test would share one instance, + # and a test mutating it would leak into the rest of the session. + provider.identity = identity or GoogleWorkspaceIdentityInfo( domain=DOMAIN, customer_id=CUSTOMER_ID, delegated_user=DELEGATED_USER, root_org_unit_id=ROOT_ORG_UNIT_ID, profile="default", - ), -): - provider = MagicMock() - provider.type = "googleworkspace" - provider.identity = identity + ) provider.domain_resource = build_googleworkspace_domain_resource() return provider diff --git a/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py b/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py index 4581ffa527..5e72d285d8 100644 --- a/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py +++ b/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py @@ -1,6 +1,13 @@ from unittest.mock import MagicMock -from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService +import pytest + +from prowler.providers.googleworkspace.lib.service.service import ( + CUSTOMER_SCOPE, + OVERRIDE_SCOPE, + UNKNOWN_SCOPE, + GoogleWorkspaceService, +) ROOT_OU_ID = "03ph8a2z1234" @@ -80,3 +87,41 @@ class TestIsCustomerLevelPolicy: ) is False ) + + +class TestPolicyScope: + @pytest.mark.parametrize( + "policy, expected", + [ + ({}, CUSTOMER_SCOPE), + ({"policyQuery": {}}, CUSTOMER_SCOPE), + ({"policyQuery": None}, CUSTOMER_SCOPE), + ({"policyQuery": {"orgUnit": ""}}, CUSTOMER_SCOPE), + ({"policyQuery": {"orgUnit": f"orgUnits/{ROOT_OU_ID}"}}, CUSTOMER_SCOPE), + ({"policyQuery": {"orgUnit": "orgUnits/sub_ou"}}, OVERRIDE_SCOPE), + ({"policyQuery": {"group": "groups/xyz"}}, OVERRIDE_SCOPE), + ( + {"policyQuery": {"group": "groups/xyz", "orgUnit": "orgUnits/sub_ou"}}, + OVERRIDE_SCOPE, + ), + ], + ) + def test_scope_with_a_known_root_org_unit(self, policy, expected): + assert _make_service()._policy_scope(policy) == expected + + @pytest.mark.parametrize("org_unit", [f"orgUnits/{ROOT_OU_ID}", "orgUnits/sub_ou"]) + def test_without_the_root_id_an_org_unit_scope_is_unknown(self, org_unit): + """The root OU and a sub-OU are indistinguishable, so neither may be assumed""" + svc = _make_service(root_org_unit_id=None) + + assert ( + svc._policy_scope({"policyQuery": {"orgUnit": org_unit}}) == UNKNOWN_SCOPE + ) + + def test_a_group_is_an_override_even_without_the_root_id(self): + svc = _make_service(root_org_unit_id=None) + + assert ( + svc._policy_scope({"policyQuery": {"group": "groups/xyz"}}) + == OVERRIDE_SCOPE + ) diff --git a/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py index de18c6d9c6..a87979ee5c 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py @@ -40,6 +40,38 @@ class TestGmailDomainSpoofingProtectionEnabled: assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID + def test_pass_enable_flag_not_returned(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + domain_spoofing_consequence="SPAM_FOLDER", + ) + + check = gmail_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "uses Google's default (enabled)" in findings[0].status_extended + assert "is enabled with action" not in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + def test_fail_no_action(self): mock_provider = set_mocked_googleworkspace_provider() @@ -70,6 +102,36 @@ class TestGmailDomainSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "no action" in findings[0].status_extended + def test_fail_warning_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_domain_name_spoofing=True, + domain_spoofing_consequence="WARNING", + ) + + check = gmail_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "show a warning" in findings[0].status_extended + def test_fail_protection_disabled(self): mock_provider = set_mocked_googleworkspace_provider() @@ -100,7 +162,7 @@ class TestGmailDomainSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "disabled" in findings[0].status_extended - def test_pass_using_default(self): + def test_fail_using_default(self): mock_provider = set_mocked_googleworkspace_provider() with ( @@ -124,8 +186,8 @@ class TestGmailDomainSpoofingProtectionEnabled: findings = check.execute() assert len(findings) == 1 - assert findings[0].status == "PASS" - assert "secure default" in findings[0].status_extended + assert findings[0].status == "FAIL" + assert "default action" in findings[0].status_extended def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py index d30284b852..c4ba415ba7 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py @@ -70,6 +70,36 @@ class TestGmailEmployeeNameSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "no action" in findings[0].status_extended + def test_fail_warning_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled import ( + gmail_employee_name_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_employee_name_spoofing=True, + employee_name_spoofing_consequence="WARNING", + ) + + check = gmail_employee_name_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "show a warning" in findings[0].status_extended + def test_fail_protection_disabled(self): mock_provider = set_mocked_googleworkspace_provider() @@ -100,7 +130,7 @@ class TestGmailEmployeeNameSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "disabled" in findings[0].status_extended - def test_pass_using_default(self): + def test_fail_using_default(self): mock_provider = set_mocked_googleworkspace_provider() with ( @@ -124,8 +154,8 @@ class TestGmailEmployeeNameSpoofingProtectionEnabled: findings = check.execute() assert len(findings) == 1 - assert findings[0].status == "PASS" - assert "secure default" in findings[0].status_extended + assert findings[0].status == "FAIL" + assert "default action" in findings[0].status_extended def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py index b06092ebe5..1decd300f2 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py @@ -103,6 +103,36 @@ class TestGmailGroupsSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "no action" in findings[0].status_extended + def test_fail_warning_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_groups_spoofing=True, + groups_spoofing_consequence="WARNING", + ) + + check = gmail_groups_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "show a warning" in findings[0].status_extended + def test_fail_protection_disabled(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py index b319b8fdb1..1677df655c 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py @@ -70,6 +70,36 @@ class TestGmailInboundDomainSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "no action" in findings[0].status_extended + def test_fail_warning_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled import ( + gmail_inbound_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_inbound_domain_spoofing=True, + inbound_domain_spoofing_consequence="WARNING", + ) + + check = gmail_inbound_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "show a warning" in findings[0].status_extended + def test_fail_protection_disabled(self): mock_provider = set_mocked_googleworkspace_provider() @@ -100,7 +130,7 @@ class TestGmailInboundDomainSpoofingProtectionEnabled: assert findings[0].status == "FAIL" assert "disabled" in findings[0].status_extended - def test_pass_using_default(self): + def test_fail_using_default(self): mock_provider = set_mocked_googleworkspace_provider() with ( @@ -124,8 +154,8 @@ class TestGmailInboundDomainSpoofingProtectionEnabled: findings = check.execute() assert len(findings) == 1 - assert findings[0].status == "PASS" - assert "secure default" in findings[0].status_extended + assert findings[0].status == "FAIL" + assert "default action" in findings[0].status_extended def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/lib/spoofing_test.py b/tests/providers/googleworkspace/services/gmail/lib/spoofing_test.py new file mode 100644 index 0000000000..34d14c8de5 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/lib/spoofing_test.py @@ -0,0 +1,34 @@ +import pytest + +from prowler.providers.googleworkspace.services.gmail.lib.spoofing import ( + PROTECTIVE_CONSEQUENCES, + describe_consequence, + is_protective, +) + + +class TestIsProtective: + @pytest.mark.parametrize("consequence", sorted(PROTECTIVE_CONSEQUENCES)) + def test_actions_that_move_the_message_out_of_the_inbox(self, consequence): + assert is_protective(consequence) is True + + @pytest.mark.parametrize( + "consequence", ["WARNING", "NO_ACTION", None, "", "spam_folder", "UNKNOWN"] + ) + def test_everything_else_is_not_protective(self, consequence): + """Anything the benchmark does not accept, including unknown values""" + assert is_protective(consequence) is False + + +class TestDescribeConsequence: + @pytest.mark.parametrize( + "consequence, expected", + [ + (None, "uses Google's default action"), + ("NO_ACTION", "is set to take no action"), + ("WARNING", "show a warning"), + ("SOMETHING_NEW", "is set to 'SOMETHING_NEW'"), + ], + ) + def test_renders_for_finding_messages(self, consequence, expected): + assert expected in describe_consequence(consequence) diff --git a/tests/providers/googleworkspace/services/rules/lib/alerts_test.py b/tests/providers/googleworkspace/services/rules/lib/alerts_test.py new file mode 100644 index 0000000000..886de269c2 --- /dev/null +++ b/tests/providers/googleworkspace/services/rules/lib/alerts_test.py @@ -0,0 +1,154 @@ +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from prowler.lib.check.models import CheckMetadata +from prowler.providers.googleworkspace.services.rules import rules_service +from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, +) +from prowler.providers.googleworkspace.services.rules.rules_service import ( + SystemDefinedAlert, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + +RULE_NAME = "Leaked password" +OTHER_RULE = "Suspicious login" + + +def make_client(alerts, policies_fetched=True): + client = MagicMock() + client.provider = set_mocked_googleworkspace_provider() + client.policies_fetched = policies_fetched + client.system_defined_alerts = alerts + return client + + +def configured(**overrides): + values = dict( + display_name=RULE_NAME, + state="ACTIVE", + severity="MEDIUM", + email_notifications_enabled=True, + all_super_admins=True, + ) + values.update(overrides) + return SystemDefinedAlert(**values) + + +# Real metadata: CheckReportGoogleWorkspace validates it, a mock will not do. +# Loaded from the file rather than from the check class, whose module import +# builds the service client and needs a live provider. +METADATA_FILE = ( + Path(rules_service.__file__).parent + / "rules_leaked_password_alert_configured" + / "rules_leaked_password_alert_configured.metadata.json" +) +METADATA = CheckMetadata.parse_file(METADATA_FILE).json() + + +def run(alerts, minimum_severity="MEDIUM", policies_fetched=True): + return evaluate_system_defined_alert( + make_client(alerts, policies_fetched), METADATA, RULE_NAME, minimum_severity + ) + + +class TestEvaluateSystemDefinedAlert: + def test_evaluates_only_the_requested_rule(self): + findings = run( + [ + configured(display_name=OTHER_RULE, state="INACTIVE", severity=None), + configured(), + configured(display_name="Government-backed attacks", severity="HIGH"), + ] + ) + + assert len(findings) == 1 + assert findings[0].resource_name == RULE_NAME + assert findings[0].status == "PASS" + + def test_no_finding_when_the_rule_is_absent(self): + assert run([configured(display_name=OTHER_RULE)]) == [] + + def test_no_finding_when_fetch_failed(self): + assert run([configured()], policies_fetched=False) == [] + + @pytest.mark.parametrize("severity", ["MEDIUM", "HIGH"]) + def test_a_severity_above_the_minimum_is_stricter_not_weaker(self, severity): + findings = run([configured(severity=severity)], "MEDIUM") + + assert findings[0].status == "PASS" + + @pytest.mark.parametrize("severity", ["CRITICAL", "high", "SEVERITY_UNSPECIFIED"]) + def test_an_unrankable_severity_is_not_claimed_to_be_below_the_minimum( + self, severity + ): + """Saying CRITICAL falls short of MEDIUM would be a lie, not a finding""" + findings = run([configured(severity=severity)], "MEDIUM") + + assert findings[0].status == "FAIL" + assert f"severity is {severity}, which is not one of" in ( + findings[0].status_extended + ) + assert f"should be at least {severity}" not in findings[0].status_extended + + def test_reports_the_minimum_severity_on_failure(self): + findings = run([configured(severity="LOW")], "MEDIUM") + + assert findings[0].status == "FAIL" + assert "severity is LOW (should be at least MEDIUM)" in ( + findings[0].status_extended + ) + + def test_an_unobserved_rule_left_on_an_active_default_is_manual(self): + """Google documents no default severity, so it cannot be verified""" + findings = run([configured(severity=None, from_default=True)]) + + assert findings[0].status == "MANUAL" + assert "was not returned by the API" in findings[0].status_extended + + def test_an_unobserved_rule_that_defaults_to_off_still_fails(self): + """The OFF default is documented, so it fails whatever the severity is""" + findings = run([configured(state="INACTIVE", severity=None, from_default=True)]) + + assert findings[0].status == "FAIL" + assert "Google's default for it is OFF" in findings[0].status_extended + + def test_fail_when_the_alert_is_not_sent_to_the_alert_center(self): + """An active rule whose alert center delivery is DISABLED is not compliant""" + findings = run([configured(alert_center_status="DISABLED")], "MEDIUM") + + assert findings[0].status == "FAIL" + assert "not sent to the alert center" in findings[0].status_extended + + def test_pass_when_the_alert_center_status_is_not_reported(self): + """The API never returns this field, so its absence cannot fail a rule""" + findings = run([configured(alert_center_status=None)], "MEDIUM") + + assert findings[0].status == "PASS" + + def test_a_failing_condition_wins_over_an_unreported_delivery(self): + findings = run([configured(severity="LOW", alert_center_status=None)], "MEDIUM") + + assert findings[0].status == "FAIL" + assert "severity is LOW" in findings[0].status_extended + + def test_reports_every_failing_condition(self): + findings = run( + [ + configured( + state="INACTIVE", + severity="LOW", + email_notifications_enabled=False, + ) + ] + ) + + extended = findings[0].status_extended + assert findings[0].status == "FAIL" + assert "alert is OFF" in extended + assert "email notifications are disabled" in extended + assert "severity is LOW" in extended diff --git a/tests/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured_test.py index cf1a6128c4..c3c83fd019 100644 --- a/tests/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_admin_privilege_granted_alert_configured/rules_admin_privilege_granted_alert_configured_test.py @@ -46,7 +46,79 @@ class TestRulesAdminPrivilegeGrantedAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_below_the_minimum(self): + 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.rules.rules_admin_privilege_granted_alert_configured.rules_admin_privilege_granted_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_admin_privilege_granted_alert_configured.rules_admin_privilege_granted_alert_configured import ( + rules_admin_privilege_granted_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="LOW", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_admin_privilege_granted_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is LOW" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_admin_privilege_granted_alert_configured.rules_admin_privilege_granted_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_admin_privilege_granted_alert_configured.rules_admin_privilege_granted_alert_configured import ( + rules_admin_privilege_granted_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_admin_privilege_granted_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): diff --git a/tests/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured_test.py index f907b2f89e..82ced6a8d1 100644 --- a/tests/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_gmail_employee_spoofing_alert_configured/rules_gmail_employee_spoofing_alert_configured_test.py @@ -46,7 +46,79 @@ class TestRulesGmailEmployeeSpoofingAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_below_the_minimum(self): + 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.rules.rules_gmail_employee_spoofing_alert_configured.rules_gmail_employee_spoofing_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_gmail_employee_spoofing_alert_configured.rules_gmail_employee_spoofing_alert_configured import ( + rules_gmail_employee_spoofing_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="LOW", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_gmail_employee_spoofing_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is LOW" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_gmail_employee_spoofing_alert_configured.rules_gmail_employee_spoofing_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_gmail_employee_spoofing_alert_configured.rules_gmail_employee_spoofing_alert_configured import ( + rules_gmail_employee_spoofing_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_gmail_employee_spoofing_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): diff --git a/tests/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured_test.py index 90ec845258..0bc9fd4158 100644 --- a/tests/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_government_backed_attacks_alert_configured/rules_government_backed_attacks_alert_configured_test.py @@ -35,7 +35,7 @@ class TestRulesGovernmentBackedAttacksAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="HIGH", email_notifications_enabled=True, all_super_admins=True, ) @@ -46,7 +46,79 @@ class TestRulesGovernmentBackedAttacksAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_below_the_minimum(self): + 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.rules.rules_government_backed_attacks_alert_configured.rules_government_backed_attacks_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_government_backed_attacks_alert_configured.rules_government_backed_attacks_alert_configured import ( + rules_government_backed_attacks_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="MEDIUM", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_government_backed_attacks_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is MEDIUM" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_government_backed_attacks_alert_configured.rules_government_backed_attacks_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_government_backed_attacks_alert_configured.rules_government_backed_attacks_alert_configured import ( + rules_government_backed_attacks_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_government_backed_attacks_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): @@ -105,7 +177,7 @@ class TestRulesGovernmentBackedAttacksAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="HIGH", email_notifications_enabled=False, all_super_admins=False, ) @@ -141,7 +213,7 @@ class TestRulesGovernmentBackedAttacksAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="HIGH", email_notifications_enabled=True, all_super_admins=False, ) diff --git a/tests/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured_test.py index ace6c29d13..450e825309 100644 --- a/tests/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_leaked_password_alert_configured/rules_leaked_password_alert_configured_test.py @@ -46,7 +46,116 @@ class TestRulesLeakedPasswordAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_high_severity(self): + """Test PASS with High severity: CIS 6.7 sets High in the remediation and Medium in the audit.""" + 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.rules.rules_leaked_password_alert_configured.rules_leaked_password_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_leaked_password_alert_configured.rules_leaked_password_alert_configured import ( + rules_leaked_password_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="HIGH", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_leaked_password_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_below_the_minimum(self): + 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.rules.rules_leaked_password_alert_configured.rules_leaked_password_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_leaked_password_alert_configured.rules_leaked_password_alert_configured import ( + rules_leaked_password_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="LOW", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_leaked_password_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is LOW" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_leaked_password_alert_configured.rules_leaked_password_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_leaked_password_alert_configured.rules_leaked_password_alert_configured import ( + rules_leaked_password_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_leaked_password_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): diff --git a/tests/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured_test.py index db1b8056e7..9e2bc07c20 100644 --- a/tests/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_password_changed_alert_configured/rules_password_changed_alert_configured_test.py @@ -46,7 +46,79 @@ class TestRulesPasswordChangedAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_below_the_minimum(self): + 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.rules.rules_password_changed_alert_configured.rules_password_changed_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_password_changed_alert_configured.rules_password_changed_alert_configured import ( + rules_password_changed_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="LOW", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_password_changed_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is LOW" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_password_changed_alert_configured.rules_password_changed_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_password_changed_alert_configured.rules_password_changed_alert_configured import ( + rules_password_changed_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_password_changed_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): diff --git a/tests/providers/googleworkspace/services/rules/rules_service_test.py b/tests/providers/googleworkspace/services/rules/rules_service_test.py index 6368df4bcb..0d0fd39ed4 100644 --- a/tests/providers/googleworkspace/services/rules/rules_service_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_service_test.py @@ -1,9 +1,19 @@ +from pathlib import Path from unittest.mock import MagicMock, patch +from prowler.lib.check.models import CheckMetadata +from prowler.providers.googleworkspace.services.rules import rules_service from tests.providers.googleworkspace.googleworkspace_fixtures import ( set_mocked_googleworkspace_provider, ) +METADATA_FILE = ( + Path(rules_service.__file__).parent + / "rules_government_backed_attacks_alert_configured" + / "rules_government_backed_attacks_alert_configured.metadata.json" +) +METADATA = CheckMetadata.parse_file(METADATA_FILE).json() + class TestRulesService: def test_fetch_fully_configured_rule(self): @@ -28,7 +38,10 @@ class TestRulesService: "action": { "alertCenterAction": { "recipients": [{"allSuperAdmins": True}], - "alertCenterConfig": {"severity": "LOW"}, + "alertCenterConfig": { + "severity": "LOW", + "status": "ENABLED", + }, } }, "state": "ACTIVE", @@ -68,6 +81,7 @@ class TestRulesService: assert suspicious_login.email_notifications_enabled is True assert suspicious_login.all_super_admins is True assert suspicious_login.severity == "LOW" + assert suspicious_login.alert_center_status == "ENABLED" def test_fetch_rule_without_email_notifications(self): """Test a rule that is ACTIVE but has no email recipients configured.""" @@ -204,6 +218,114 @@ class TestRulesService: assert gov_attacks.email_notifications_enabled is True assert gov_attacks.all_super_admins is True + def test_ou_and_group_scoped_policies_are_skipped(self): + """Only the customer-level policy describes the whole domain""" + 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 + + def alert_policy(display_name, state, policy_query=None): + policy = { + "setting": { + "type": "settings/rule.system_defined_alerts", + "value": {"displayName": display_name, "state": state}, + } + } + if policy_query: + policy["policyQuery"] = policy_query + return policy + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + alert_policy("Suspicious login", "ACTIVE"), + alert_policy( + "Suspicious login", "INACTIVE", {"orgUnit": "orgUnits/sales_team"} + ), + alert_policy( + "Leaked password", "INACTIVE", {"group": "groups/contractors"} + ), + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.rules.rules_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.rules.rules_service import ( + Rules, + ) + + rules = Rules(mock_provider) + + by_name = {a.display_name: a for a in rules.system_defined_alerts} + assert by_name["Suspicious login"].state == "ACTIVE" + assert by_name["Suspicious login"].from_default is False + # Only seen in a group-scoped policy, so it falls back to the default. + assert by_name["Leaked password"].from_default is True + + def test_empty_response_marks_alerts_as_inferred(self): + """A rule the API never returned must not be reported as tenant configuration.""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = {"policies": []} + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.rules.rules_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.rules.lib.alerts import ( + evaluate_system_defined_alert, + ) + from prowler.providers.googleworkspace.services.rules.rules_service import ( + Rules, + ) + + rules = Rules(mock_provider) + + assert all(alert.from_default for alert in rules.system_defined_alerts) + + # End to end: nothing was observed for a rule that defaults to ON, + # so it has to be reviewed by hand instead of blamed on the tenant. + client = MagicMock() + client.provider = mock_provider + client.policies_fetched = True + client.system_defined_alerts = rules.system_defined_alerts + findings = evaluate_system_defined_alert( + client, METADATA, "Government-backed attacks", "HIGH" + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "was not returned by the API" in findings[0].status_extended + def test_api_error_sets_policies_fetched_false(self): """Test that API errors result in policies_fetched being False.""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured_test.py index acd60756f0..58b8a273f1 100644 --- a/tests/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_suspicious_activity_suspension_alert_configured/rules_suspicious_activity_suspension_alert_configured_test.py @@ -35,7 +35,7 @@ class TestRulesSuspiciousActivitySuspensionAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="HIGH", email_notifications_enabled=True, all_super_admins=True, ) @@ -46,7 +46,79 @@ class TestRulesSuspiciousActivitySuspensionAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_below_the_minimum(self): + 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.rules.rules_suspicious_activity_suspension_alert_configured.rules_suspicious_activity_suspension_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_suspicious_activity_suspension_alert_configured.rules_suspicious_activity_suspension_alert_configured import ( + rules_suspicious_activity_suspension_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="LOW", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_suspicious_activity_suspension_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is LOW" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_suspicious_activity_suspension_alert_configured.rules_suspicious_activity_suspension_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_suspicious_activity_suspension_alert_configured.rules_suspicious_activity_suspension_alert_configured import ( + rules_suspicious_activity_suspension_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_suspicious_activity_suspension_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): @@ -105,7 +177,7 @@ class TestRulesSuspiciousActivitySuspensionAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="HIGH", email_notifications_enabled=False, all_super_admins=False, ) @@ -141,7 +213,7 @@ class TestRulesSuspiciousActivitySuspensionAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="HIGH", email_notifications_enabled=True, all_super_admins=False, ) diff --git a/tests/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured_test.py index aab70797dc..1c6f568421 100644 --- a/tests/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_suspicious_login_alert_configured/rules_suspicious_login_alert_configured_test.py @@ -16,6 +16,42 @@ class TestRulesSuspiciousLoginAlertConfigured: """Test PASS when alert is ON, email notifications ON, recipients = all super admins.""" 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.rules.rules_suspicious_login_alert_configured.rules_suspicious_login_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_suspicious_login_alert_configured.rules_suspicious_login_alert_configured import ( + rules_suspicious_login_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="LOW", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_suspicious_login_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_severity_above_the_minimum(self): + mock_provider = set_mocked_googleworkspace_provider() + with ( patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -46,7 +82,43 @@ class TestRulesSuspiciousLoginAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_suspicious_login_alert_configured.rules_suspicious_login_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_suspicious_login_alert_configured.rules_suspicious_login_alert_configured import ( + rules_suspicious_login_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_suspicious_login_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): @@ -105,7 +177,7 @@ class TestRulesSuspiciousLoginAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="LOW", email_notifications_enabled=False, all_super_admins=False, ) @@ -141,7 +213,7 @@ class TestRulesSuspiciousLoginAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="LOW", email_notifications_enabled=True, all_super_admins=False, ) diff --git a/tests/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured_test.py b/tests/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured_test.py index f1596ace67..35c51dbd59 100644 --- a/tests/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured_test.py +++ b/tests/providers/googleworkspace/services/rules/rules_suspicious_programmatic_login_alert_configured/rules_suspicious_programmatic_login_alert_configured_test.py @@ -35,7 +35,7 @@ class TestRulesSuspiciousProgrammaticLoginAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="LOW", email_notifications_enabled=True, all_super_admins=True, ) @@ -46,7 +46,79 @@ class TestRulesSuspiciousProgrammaticLoginAlertConfigured: assert len(findings) == 1 assert findings[0].status == "PASS" - assert "properly configured" in findings[0].status_extended + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_severity_above_the_minimum(self): + 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.rules.rules_suspicious_programmatic_login_alert_configured.rules_suspicious_programmatic_login_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_suspicious_programmatic_login_alert_configured.rules_suspicious_programmatic_login_alert_configured import ( + rules_suspicious_programmatic_login_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + severity="HIGH", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_suspicious_programmatic_login_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "is properly configured" in findings[0].status_extended + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_severity_not_configured(self): + """Test FAIL when the alert is on but no severity is 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.rules.rules_suspicious_programmatic_login_alert_configured.rules_suspicious_programmatic_login_alert_configured.rules_client" + ) as mock_rules_client, + ): + from prowler.providers.googleworkspace.services.rules.rules_suspicious_programmatic_login_alert_configured.rules_suspicious_programmatic_login_alert_configured import ( + rules_suspicious_programmatic_login_alert_configured, + ) + + mock_rules_client.provider = mock_provider + mock_rules_client.policies_fetched = True + mock_rules_client.system_defined_alerts = [ + SystemDefinedAlert( + display_name=RULE_NAME, + state="ACTIVE", + email_notifications_enabled=True, + all_super_admins=True, + ) + ] + + check = rules_suspicious_programmatic_login_alert_configured() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "severity is not configured" in findings[0].status_extended assert findings[0].customer_id == CUSTOMER_ID def test_fail_alert_off(self): @@ -105,7 +177,7 @@ class TestRulesSuspiciousProgrammaticLoginAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="LOW", email_notifications_enabled=False, all_super_admins=False, ) @@ -141,7 +213,7 @@ class TestRulesSuspiciousProgrammaticLoginAlertConfigured: SystemDefinedAlert( display_name=RULE_NAME, state="ACTIVE", - severity="MEDIUM", + severity="LOW", email_notifications_enabled=True, all_super_admins=False, ) diff --git a/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py b/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py index 13335e22cb..e17614de0d 100644 --- a/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py +++ b/tests/providers/googleworkspace/services/security/googleworkspace_security_service_test.py @@ -1,6 +1,9 @@ from unittest.mock import MagicMock, patch +import pytest + from tests.providers.googleworkspace.googleworkspace_fixtures import ( + ROOT_ORG_UNIT_ID, set_mocked_googleworkspace_provider, ) @@ -280,6 +283,141 @@ class TestSecurityService: assert security.policies.trust_internal_apps is None assert security.policies.dlp_drive_rules_exist is None + def test_group_and_sub_ou_policies_are_recorded_as_overrides(self): + """The customer-level value is not what the overridden users get""" + 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": [ + { + "policyQuery": {"group": "groups/abc123"}, + "setting": { + "type": "settings/security.two_step_verification_enforcement", + "value": {"enforcedFrom": "1970-01-01T00:00:00Z"}, + }, + }, + { + "policyQuery": {"orgUnit": "orgUnits/03ph8a2z1xdnme9"}, + "setting": { + "type": "settings/security.two_step_verification_enforcement_factor", + "value": {"allowedSignInFactorSet": "ALL"}, + }, + }, + { + "setting": { + "type": "settings/security.two_step_verification_enforcement", + "value": {"enforcedFrom": "2026-05-25T15:27:52.352Z"}, + } + }, + ] + } + 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.overridden_settings == [ + "security.two_step_verification_enforcement", + "security.two_step_verification_enforcement_factor", + ] + assert security.policies.unresolved_scope is False + # The customer-level policy is still read, it is just not effective + # for everyone. + assert security.policies.two_sv_enforced_from == "2026-05-25T15:27:52.352Z" + + @pytest.mark.parametrize( + "root_org_unit_id, org_unit, expected_enforced_from, expected_unresolved", + [ + # The root OU is the whole domain: read it, do not call it an override. + ( + ROOT_ORG_UNIT_ID, + f"orgUnits/{ROOT_ORG_UNIT_ID}", + "2026-05-25T15:27:52.352Z", + False, + ), + # Without the root id a sub-OU cannot be told apart from the root, so + # the value is dropped and the scope is flagged as unresolved. + (None, "orgUnits/03ph8a2z1xdnme9", None, True), + ], + ) + def test_an_org_unit_is_never_reported_as_an_override( + self, root_org_unit_id, org_unit, expected_enforced_from, expected_unresolved + ): + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_provider.identity = mock_provider.identity.copy( + update={"root_org_unit_id": root_org_unit_id} + ) + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies = MagicMock() + mock_policies.execute.return_value = { + "policies": [ + { + "policyQuery": {"orgUnit": org_unit}, + "setting": { + "type": "settings/security.two_step_verification_enforcement", + "value": {"enforcedFrom": "2026-05-25T15:27:52.352Z"}, + }, + } + ] + } + mock_service.policies().list.side_effect = [ + mock_policies, + mock_policies, + mock_policies, + ] + 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.overridden_settings == [] + assert security.policies.two_sv_enforced_from == expected_enforced_from + assert security.policies.unresolved_scope is expected_unresolved + def test_fetch_policies_api_error(self): """Test handling of API errors during policy fetch""" mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/security/lib/durations_test.py b/tests/providers/googleworkspace/services/security/lib/durations_test.py new file mode 100644 index 0000000000..7e1a6d3038 --- /dev/null +++ b/tests/providers/googleworkspace/services/security/lib/durations_test.py @@ -0,0 +1,97 @@ +from datetime import datetime, timezone + +import pytest + +from prowler.providers.googleworkspace.services.security.lib.durations import ( + ONE_DAY_SECONDS, + ONE_YEAR_SECONDS, + TWO_WEEKS_SECONDS, + enforcement_issue, + format_duration, + parse_duration_seconds, +) + + +class TestParseDurationSeconds: + @pytest.mark.parametrize( + "value, expected", + [ + ("0s", 0), + ("86400s", ONE_DAY_SECONDS), + ("1209600s", TWO_WEEKS_SECONDS), + ("31536000s", ONE_YEAR_SECONDS), + ("86400.9s", ONE_DAY_SECONDS), + (" 86400s ", ONE_DAY_SECONDS), + ], + ) + def test_parses_protobuf_durations(self, value, expected): + assert parse_duration_seconds(value) == expected + + @pytest.mark.parametrize( + "value", + [None, "", "86400", "28d", "P30D", "-3600s", "1e5s", "abc", 86400], + ) + def test_returns_none_for_anything_it_cannot_read(self, value): + """Callers must be able to tell a real length from an unreadable value""" + assert parse_duration_seconds(value) is None + + +class TestFormatDuration: + @pytest.mark.parametrize( + "value, expected", + [ + ("0s", "none"), + ("86400s", "1 day(s)"), + ("1209600s", "14 day(s)"), + ("31536000s", "365 day(s)"), + ("3600s", "1 hour(s)"), + ("129600s", "36 hour(s)"), + ("5400s", "5400 second(s)"), + (None, "not configured"), + ("28d", "not configured"), + ], + ) + def test_renders_for_finding_messages(self, value, expected): + assert format_duration(value) == expected + + +class TestEnforcementIssue: + NOW = datetime(2026, 8, 25, 12, 0, 0, tzinfo=timezone.utc) + + @pytest.mark.parametrize( + "value", ["2026-05-25T15:27:52.352Z", "2026-08-25T11:59:59Z"] + ) + def test_no_issue_once_enforcement_has_started(self, value): + assert enforcement_issue(value, now=self.NOW) is None + + def test_naive_timestamp_is_read_as_utc(self): + assert enforcement_issue("2026-01-01T00:00:00", now=self.NOW) is None + + @pytest.mark.parametrize("value", [None, ""]) + def test_missing_value_defaults_to_off(self, value): + assert ( + enforcement_issue(value, now=self.NOW) + == "enforcement is not configured and defaults to OFF" + ) + + @pytest.mark.parametrize( + "value", ["1970-01-01T00:00:00Z", "1970-01-01T00:00:00.000000000Z"] + ) + def test_zero_value_timestamp_is_off(self, value): + """The API reports OFF as the protobuf zero-value Timestamp""" + assert enforcement_issue(value, now=self.NOW) == "enforcement is set to OFF" + + @pytest.mark.parametrize( + "value", ["2099-01-01T00:00:00Z", "2026-12-31T23:59:59+00:00"] + ) + def test_future_start_date_means_nobody_is_enforced_yet(self, value): + assert ( + enforcement_issue(value, now=self.NOW) + == f"enforcement does not start until {value}" + ) + + def test_unreadable_timestamp_is_reported_instead_of_assumed_active(self): + assert ( + enforcement_issue("not-a-date", now=self.NOW) + == "the enforcement start date 'not-a-date' could not be read" + ) 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 index a6d6a549a9..1da5b1bcc8 100644 --- 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 @@ -1,5 +1,7 @@ from unittest.mock import patch +import pytest + from prowler.providers.googleworkspace.services.security.security_service import ( SecurityPolicies, ) @@ -8,149 +10,264 @@ from tests.providers.googleworkspace.googleworkspace_fixtures import ( set_mocked_googleworkspace_provider, ) +CHECK_CLIENT = ( + "prowler.providers.googleworkspace.services.security." + "security_2sv_enforced.security_2sv_enforced.security_client" +) + +# A domain that satisfies every step of the CIS audit procedure. +COMPLIANT = dict( + two_sv_enforced_from="2026-05-25T15:27:52.352Z", + two_sv_allow_enrollment=True, + two_sv_enrollment_grace_period="1209600s", + two_sv_allow_trusting_device=False, + # Security keys exclude verification codes via text and phone call. + two_sv_allowed_factor_set="PASSKEY_ONLY", +) + + +def run_check(policies_fetched=True, **overrides): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch(CHECK_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 = policies_fetched + mock_client.policies = SecurityPolicies(**overrides) + + return security_2sv_enforced().execute() + class TestSecurity2svEnforced: - def test_pass_2sv_enforced(self): - """Test PASS when 2-Step Verification enforcement is active""" - mock_provider = set_mocked_googleworkspace_provider() + def test_pass_full_audit_procedure_met(self): + """PASS when every step of the CIS audit procedure is satisfied""" + findings = run_check(**COMPLIANT) - with ( - patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=mock_provider, + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "is enforced" 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 + + @pytest.mark.parametrize( + "factor_set", ["NO_TELEPHONY", "PASSKEY_ONLY", "PASSKEY_PLUS_SECURITY_CODE"] + ) + def test_pass_any_method_that_excludes_telephony(self, factor_set): + """CIS asks for any method except verification codes via text or phone call""" + findings = run_check(**{**COMPLIANT, "two_sv_allowed_factor_set": factor_set}) + + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_pass_no_enrollment_period(self): + """PASS when there is no enrollment period, which is stricter than 2 weeks""" + findings = run_check(**{**COMPLIANT, "two_sv_enrollment_grace_period": "0s"}) + + assert len(findings) == 1 + assert findings[0].status == "PASS" + + @pytest.mark.parametrize( + "overrides, expected", + [ + ({"two_sv_enforced_from": None}, "not configured"), + ({"two_sv_enforced_from": ""}, "not configured"), + ({"two_sv_enforced_from": "1970-01-01T00:00:00Z"}, "OFF"), + ({"two_sv_allow_enrollment": False}, "not allowed to turn on"), + ( + {"two_sv_enrollment_grace_period": "2592000s"}, + "enrollment period is 30 day(s)", ), - 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, + ({"two_sv_allow_trusting_device": True}, "trust their device"), + ( + {"two_sv_allow_trusting_device": None}, + "device trust is not configured", ), - 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, + ( + {"two_sv_enforced_from": "2099-01-01T00:00:00Z"}, + "enforcement does not start until 2099-01-01T00:00:00Z", ), - 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, + ( + {"two_sv_enforced_from": "not-a-date"}, + "enforcement start date 'not-a-date' could not be read", ), - 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, - ) + ( + {"two_sv_enrollment_grace_period": "28d"}, + "enrollment period '28d' could not be read", + ), + ( + {"two_sv_allowed_factor_set": "ALL"}, + "the allowed methods are ALL", + ), + ( + {"two_sv_allowed_factor_set": "SOME_FUTURE_ENUM"}, + "the allowed methods are SOME_FUTURE_ENUM", + ), + ( + {"two_sv_allowed_factor_set": None}, + "allowed methods are not configured", + ), + ], + ) + def test_fail_each_audit_step(self, overrides, expected): + """FAIL when any single step of the CIS audit procedure is not met""" + findings = run_check(**{**COMPLIANT, **overrides}) - mock_client.provider = mock_provider - mock_client.policies_fetched = True - mock_client.policies = SecurityPolicies( - two_sv_enforced_from="1970-01-01T00:00:00Z" - ) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert expected in findings[0].status_extended - check = security_2sv_enforced() - findings = check.execute() + def test_fail_reports_every_issue(self): + """A domain left on Google's defaults reports all the failing steps""" + findings = run_check(two_sv_enforced_from=None) - assert len(findings) == 1 - assert findings[0].status == "FAIL" - assert "OFF" in findings[0].status_extended + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enforcement is not configured" in findings[0].status_extended + assert "device trust is not configured" in findings[0].status_extended + assert "allowed methods are not configured" in findings[0].status_extended + + def test_manual_when_a_group_or_sub_ou_overrides_a_passing_policy(self): + """A passing domain-wide policy cannot be confirmed for the overridden users""" + findings = run_check( + **{ + **COMPLIANT, + "overridden_settings": {"security.two_step_verification_enforcement"}, + } + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "security.two_step_verification_enforcement is also overridden" in ( + findings[0].status_extended + ) + + def test_a_domain_wide_failure_is_reported_even_with_an_override(self): + """Whoever no override reaches still gets the failing domain-wide policy""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_enforced_from": None, + "overridden_settings": { + "security.two_step_verification_enforcement_factor" + }, + } + ) + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enforcement is not configured" in findings[0].status_extended + + def test_manual_when_several_settings_are_overridden(self): + findings = run_check( + **{ + **COMPLIANT, + "overridden_settings": [ + "security.two_step_verification_device_trust", + "security.two_step_verification_enforcement", + ], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert ( + "security.two_step_verification_device_trust, " + "security.two_step_verification_enforcement are also overridden" + ) in findings[0].status_extended + + def test_manual_when_a_setting_only_exists_below_the_domain(self): + """No domain-wide value was reported, so the defaults would fabricate issues""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_enforced_from": None, + "overridden_settings": ["security.two_step_verification_enforcement"], + "unobserved_settings": ["security.two_step_verification_enforcement"], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "no domain-wide value was reported" in findings[0].status_extended + + def test_manual_when_the_policy_scope_could_not_be_resolved(self): + """Every value was dropped, so none of them can be judged""" + findings = run_check(**{**COMPLIANT, "unresolved_scope": True}) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "root organizational unit could not be resolved" in ( + findings[0].status_extended + ) + + def test_a_failure_keeps_the_override_caveat(self): + findings = run_check( + **{ + **COMPLIANT, + "two_sv_allow_trusting_device": True, + "overridden_settings": ["security.two_step_verification_enforcement"], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "trust their device" in findings[0].status_extended + assert "also overridden" in findings[0].status_extended + + def test_manual_when_every_failing_setting_is_overridden(self): + """The overriding value is not exposed, so the failure is unconfirmed""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_allow_trusting_device": True, + "overridden_settings": ["security.two_step_verification_device_trust"], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "trust their device" in findings[0].status_extended + assert "every failing setting is also overridden" in ( + findings[0].status_extended + ) + + def test_fail_when_only_some_failing_settings_are_overridden(self): + """A failure no override reaches is still proven for the whole domain""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_allow_trusting_device": True, + "two_sv_allowed_factor_set": "ALL", + "overridden_settings": ["security.two_step_verification_device_trust"], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "the allowed methods are ALL" in findings[0].status_extended + + def test_an_override_on_a_setting_this_check_ignores_does_not_apply(self): + """The sign-in code setting belongs to 4.1.1.2, not to 4.1.1.1 or 4.1.1.3""" + findings = run_check( + **{ + **COMPLIANT, + "overridden_settings": {"security.two_step_verification_sign_in_code"}, + } + ) + + assert len(findings) == 1 + assert findings[0].status == "PASS" 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 + """No findings returned when the API fetch failed""" + assert run_check(policies_fetched=False) == [] 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 index 522164f341..6fa4de8157 100644 --- 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 @@ -1,5 +1,7 @@ from unittest.mock import patch +import pytest + from prowler.providers.googleworkspace.services.security.security_service import ( SecurityPolicies, ) @@ -8,119 +10,259 @@ from tests.providers.googleworkspace.googleworkspace_fixtures import ( set_mocked_googleworkspace_provider, ) +CHECK_CLIENT = ( + "prowler.providers.googleworkspace.services.security." + "security_2sv_hardware_keys_admins.security_2sv_hardware_keys_admins." + "security_client" +) + +# A domain that satisfies every step of the CIS audit procedure. +COMPLIANT = dict( + two_sv_allowed_factor_set="PASSKEY_ONLY", + two_sv_enforced_from="2026-05-25T15:27:52.352Z", + two_sv_allow_enrollment=True, + two_sv_backup_code_exception_period="86400s", +) + + +def run_check(policies_fetched=True, **overrides): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch(CHECK_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 = policies_fetched + mock_client.policies = SecurityPolicies(**overrides) + + return security_2sv_hardware_keys_admins().execute() + class TestSecurity2svHardwareKeysAdmins: - def test_pass_passkey_only(self): - """Test PASS when 2SV enforcement requires security keys only""" - mock_provider = set_mocked_googleworkspace_provider() + def test_pass_full_audit_procedure_met(self): + """PASS when every step of the CIS audit procedure is satisfied""" + findings = run_check(**COMPLIANT) - with ( - patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=mock_provider, + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "requires 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_pass_enforcement_scheduled_for_a_future_date(self): + """4.1.1.2 accepts 'On from ', unlike 4.1.1.1 and 4.1.1.3""" + findings = run_check( + **{**COMPLIANT, "two_sv_enforced_from": "2099-01-01T00:00:00Z"} + ) + + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_pass_no_suspension_grace_period(self): + """No grace period is stricter than the 1 day the benchmark asks for""" + findings = run_check( + **{**COMPLIANT, "two_sv_backup_code_exception_period": "0s"} + ) + + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_pass_ignores_new_user_enrollment_period(self): + """The new user enrollment period belongs to 4.1.1.1 and 4.1.1.3, not here""" + findings = run_check( + **{**COMPLIANT, "two_sv_enrollment_grace_period": "2592000s"} + ) + + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_pass_ignores_advanced_protection_security_codes(self): + """The Advanced Protection Program page is covered by CIS 4.1.3.1, not here""" + findings = run_check( + **{ + **COMPLIANT, + "advanced_protection_security_code_option": "ALLOWED_WITHOUT_REMOTE_ACCESS", + } + ) + + assert len(findings) == 1 + assert findings[0].status == "PASS" + + @pytest.mark.parametrize( + "overrides, expected", + [ + ({"two_sv_allowed_factor_set": "ALL"}, "accepted method is ALL"), + ( + {"two_sv_allowed_factor_set": None}, + "accepted method is not configured", ), - 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, + ({"two_sv_enforced_from": None}, "enforcement is not configured"), + ( + {"two_sv_enforced_from": "1970-01-01T00:00:00Z"}, + "enforcement is set to OFF", ), - 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, + ({"two_sv_allow_enrollment": False}, "not allowed to turn on"), + ( + {"two_sv_enforced_from": "not-a-date"}, + "enforcement start date 'not-a-date' could not be read", ), - 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, - ) + ( + {"two_sv_backup_code_exception_period": "1209600s"}, + "suspension grace period is 14 day(s)", + ), + ( + {"two_sv_backup_code_exception_period": "7d"}, + "suspension grace period '7d' could not be read", + ), + ], + ) + def test_fail_each_audit_step(self, overrides, expected): + """FAIL when any single step of the CIS audit procedure is not met""" + findings = run_check(**{**COMPLIANT, **overrides}) - mock_client.provider = mock_provider - mock_client.policies_fetched = True - mock_client.policies = SecurityPolicies(two_sv_allowed_factor_set=None) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert expected in findings[0].status_extended - check = security_2sv_hardware_keys_admins() - findings = check.execute() + def test_fail_keeps_domain_wide_scope_note(self): + """The domain-wide scope caveat is reported on failure too""" + findings = run_check() - assert len(findings) == 1 - assert findings[0].status == "FAIL" - assert "not configured" in findings[0].status_extended + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "role-specific 2SV enforcement" in findings[0].status_extended + + def test_manual_when_a_group_or_sub_ou_overrides_a_passing_policy(self): + """A passing domain-wide policy cannot be confirmed for the overridden users""" + findings = run_check( + **{ + **COMPLIANT, + "overridden_settings": {"security.two_step_verification_enforcement"}, + } + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "security.two_step_verification_enforcement is also overridden" in ( + findings[0].status_extended + ) + + def test_a_domain_wide_failure_is_reported_even_with_an_override(self): + """Whoever no override reaches still gets the failing domain-wide policy""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_enforced_from": None, + "overridden_settings": { + "security.two_step_verification_enforcement_factor" + }, + } + ) + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enforcement is not configured" in findings[0].status_extended + + def test_manual_when_a_setting_only_exists_below_the_domain(self): + """No domain-wide value was reported, so the defaults would fabricate issues""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_allowed_factor_set": None, + "overridden_settings": [ + "security.two_step_verification_enforcement_factor" + ], + "unobserved_settings": [ + "security.two_step_verification_enforcement_factor" + ], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "no domain-wide value was reported" in findings[0].status_extended + + def test_manual_when_the_policy_scope_could_not_be_resolved(self): + """Every value was dropped, so none of them can be judged""" + findings = run_check(**{**COMPLIANT, "unresolved_scope": True}) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "root organizational unit could not be resolved" in ( + findings[0].status_extended + ) + + def test_a_failure_keeps_the_override_caveat(self): + findings = run_check( + **{ + **COMPLIANT, + "two_sv_allowed_factor_set": "ALL", + "overridden_settings": ["security.two_step_verification_enforcement"], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "accepted method is ALL" in findings[0].status_extended + assert "also overridden" in findings[0].status_extended + + def test_manual_when_every_failing_setting_is_overridden(self): + """The admin group may get the overriding value, which is not exposed""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_allowed_factor_set": "ALL", + "overridden_settings": [ + "security.two_step_verification_enforcement_factor" + ], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "accepted method is ALL" in findings[0].status_extended + assert "administrative accounts may be configured correctly" in ( + findings[0].status_extended + ) + + def test_fail_when_only_some_failing_settings_are_overridden(self): + """A failure no override reaches is still proven for the whole domain""" + findings = run_check( + **{ + **COMPLIANT, + "two_sv_allowed_factor_set": "ALL", + "two_sv_allow_enrollment": False, + "overridden_settings": [ + "security.two_step_verification_enforcement_factor" + ], + } + ) + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not allowed to turn on" in findings[0].status_extended + + def test_an_override_on_a_setting_this_check_ignores_does_not_apply(self): + """Device trust belongs to 4.1.1.1 and 4.1.1.3, not to 4.1.1.2""" + findings = run_check( + **{ + **COMPLIANT, + "overridden_settings": {"security.two_step_verification_device_trust"}, + } + ) + + assert len(findings) == 1 + assert findings[0].status == "PASS" 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 + """No findings returned when the API fetch failed""" + assert run_check(policies_fetched=False) == [] 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 index 850bf243a9..7f504d2293 100644 --- 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 @@ -115,8 +115,8 @@ class TestSecurityPasswordPolicyStrong: 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""" + def test_pass_strength_unset_is_googles_secure_default(self): + """CIS documents 'Enforce strong password' as checked out of the box""" mock_provider = set_mocked_googleworkspace_provider() with ( @@ -146,8 +146,7 @@ class TestSecurityPasswordPolicyStrong: findings = check.execute() assert len(findings) == 1 - assert findings[0].status == "FAIL" - assert "password strength is not configured" in findings[0].status_extended + assert findings[0].status == "PASS" def test_fail_min_length_unset_reports_not_configured(self): """Test FAIL message uses 'not configured' when password_minimum_length is None""" @@ -183,6 +182,207 @@ class TestSecurityPasswordPolicyStrong: assert findings[0].status == "FAIL" assert "minimum length is not configured" in findings[0].status_extended + def test_fail_expiration_longer_than_365_days(self): + """Test FAIL when the password reset frequency exceeds 365 days""" + 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="63072000s", + ) + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "expiration is 730 day(s)" in findings[0].status_extended + assert "requires 365 days or less" in findings[0].status_extended + + def test_pass_expiration_shorter_than_365_days(self): + """Test PASS when the reset frequency is shorter, which is more restrictive""" + 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="7776000s", + ) + + check = security_password_policy_strong() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "expiration 90 day(s)" in findings[0].status_extended + + def test_fail_reuse_allowed(self): + """Test FAIL when password reuse is allowed (CIS 4.1.5.1 step 7)""" + 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=True, + password_enforce_at_login=True, + password_expiration_duration="31536000s", + ) + + findings = security_password_policy_strong().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "password reuse is allowed" in findings[0].status_extended + + def test_fail_not_enforced_at_next_sign_in(self): + """Test FAIL when the policy is not enforced at next sign-in (CIS 4.1.5.1 step 6)""" + 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=False, + password_expiration_duration="31536000s", + ) + + findings = security_password_policy_strong().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not enforced at next sign-in" in findings[0].status_extended + + def test_fail_passwords_never_expire(self): + """Test FAIL naming Google's 'Never expires' rather than '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_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="0s", + ) + + findings = security_password_policy_strong().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "never expire" in findings[0].status_extended + + def test_fail_unreadable_expiration(self): + """Test FAIL when the expiration value cannot be parsed, instead of skipping it""" + 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="365d", + ) + + findings = security_password_policy_strong().execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "could not be read" 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()