mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
feat(compliance): add CIS 1.4 google workspace compliance (#12513)
Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
This commit is contained in:
co-authored by
Lydia Vilchez
parent
8d60f9703a
commit
fb7064401b
@@ -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
|
||||
)
|
||||
|
||||
+65
-3
@@ -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()
|
||||
|
||||
+33
-3
@@ -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()
|
||||
|
||||
+30
@@ -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()
|
||||
|
||||
|
||||
+33
-3
@@ -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
|
||||
+73
-1
@@ -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):
|
||||
|
||||
+73
-1
@@ -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):
|
||||
|
||||
+76
-4
@@ -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,
|
||||
)
|
||||
|
||||
+110
-1
@@ -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):
|
||||
|
||||
+73
-1
@@ -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()
|
||||
|
||||
+76
-4
@@ -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,
|
||||
)
|
||||
|
||||
+75
-3
@@ -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,
|
||||
)
|
||||
|
||||
+76
-4
@@ -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,
|
||||
)
|
||||
|
||||
+138
@@ -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"
|
||||
)
|
||||
+251
-134
@@ -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) == []
|
||||
|
||||
+247
-105
@@ -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) == []
|
||||
|
||||
+204
-4
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user