feat(compliance): add CIS 1.4 google workspace compliance (#12513)

Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
This commit is contained in:
Pedro Martín
2026-09-01 09:35:39 +02:00
committed by GitHub
co-authored by Lydia Vilchez
parent 8d60f9703a
commit fb7064401b
73 changed files with 4941 additions and 868 deletions
@@ -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
@@ -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
)
@@ -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()
@@ -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()
@@ -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()
@@ -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()
@@ -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)
@@ -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
@@ -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):
@@ -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):
@@ -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,
)
@@ -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):
@@ -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):
@@ -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()
@@ -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,
)
@@ -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,
)
@@ -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,
)
@@ -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()
@@ -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"
)
@@ -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) == []
@@ -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 <date>', 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) == []
@@ -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()