From 638582489eb6e4a124dd11eb89f21ab3c642905d Mon Sep 17 00:00:00 2001 From: Roy Lane Date: Thu, 5 Dec 2024 14:27:58 -0500 Subject: [PATCH] commoncontrols: implement 1.1 - 1.3 & 16.2 --- .../commoncontrols/commoncontrols16_test.rego | 124 +++------- .../commoncontrols_api01_test.rego | 151 ++++++++++++ .../commoncontrols_api04_test.rego | 4 +- .../commoncontrols_api16_test.rego | 66 ++++++ scubagoggles/baselines/commoncontrols.md | 8 +- scubagoggles/rego/Commoncontrols.rego | 223 ++++++++++++++---- scubagoggles/rego/Utils.rego | 17 ++ 7 files changed, 449 insertions(+), 144 deletions(-) create mode 100644 scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api01_test.rego create mode 100644 scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api16_test.rego diff --git a/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols16_test.rego b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols16_test.rego index f251cf5c..9279c075 100644 --- a/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols16_test.rego +++ b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols16_test.rego @@ -1,5 +1,11 @@ package commoncontrols + import future.keywords +import data.utils.FailTestBothNonCompliant +import data.utils.FailTestGroupNonCompliant +import data.utils.FailTestNoEvent +import data.utils.FailTestOUNonCompliant +import data.utils.PassTestResult # # GWS.COMMONCONTROLS.16.1 @@ -27,11 +33,7 @@ test_Unlisted_Correct_V1 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == "Requirement met in all OUs and groups." + PassTestResult(PolicyId, Output) } test_Unlisted_Correct_V2 if { @@ -67,11 +69,7 @@ test_Unlisted_Correct_V2 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == "Requirement met in all OUs and groups." + PassTestResult(PolicyId, Output) } test_Unlisted_Incorrect_V1 if { @@ -95,15 +93,9 @@ test_Unlisted_Incorrect_V1 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - not RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == concat("", [ - "The following OUs are non-compliant:" - ]) + failedOU := [{"Name": "Test Top-Level OU", + "Value": NonComplianceMessage16_1}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) } test_Unlisted_Incorrect_V2 if { @@ -118,15 +110,7 @@ test_Unlisted_Incorrect_V2 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - not RuleOutput[0].RequirementMet - RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == concat("", [ - "No relevant event in the current logs for the top-level OU, Test Top-Level OU. While we are unable ", - "to determine the state from the logs, the default setting ", - "is non-compliant; manual check recommended." - ]) + FailTestNoEvent(PolicyId, Output, "Test Top-Level OU", false) } #-- @@ -156,11 +140,7 @@ test_EarlyAccessApps_OUs_Correct_V1 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == "Requirement met in all OUs and groups." + PassTestResult(PolicyId, Output) } test_EarlyAccessApps_OUs_Correct_V2 if { @@ -196,11 +176,7 @@ test_EarlyAccessApps_OUs_Correct_V2 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == "Requirement met in all OUs and groups." + PassTestResult(PolicyId, Output) } test_EarlyAccessApps_OUs_Incorrect_V1 if { @@ -225,15 +201,9 @@ test_EarlyAccessApps_OUs_Incorrect_V1 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - not RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == concat("", [ - "The following OUs are non-compliant:" - ]) + failedOU := [{"Name": "Test Top-Level OU", + "Value": NonComplianceMessage16_2}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) } test_EarlyAccessApps_OUs_Incorrect_V2 if { @@ -269,15 +239,9 @@ test_EarlyAccessApps_OUs_Incorrect_V2 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - not RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == concat("", [ - "The following OUs are non-compliant:" - ]) + failedOU := [{"Name": "Test Second-Level OU", + "Value": NonComplianceMessage16_2}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) } test_EarlyAccessApps_OUs_Correct_Groups_Incorrect_V1 if { @@ -313,15 +277,9 @@ test_EarlyAccessApps_OUs_Correct_Groups_Incorrect_V1 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - not RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == concat("", [ - "The following groups are non-compliant:" - ]) + failedGroup := [{"Name": "Test Group 1", + "Value": NonComplianceMessage16_2}] + FailTestGroupNonCompliant(PolicyId, Output, failedGroup) } test_EarlyAccessApps_OUs_Correct_Groups_Incorrect_V2 if { @@ -368,16 +326,11 @@ test_EarlyAccessApps_OUs_Correct_Groups_Incorrect_V2 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - not RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == concat("", [ - "The following groups are non-compliant:" - ]) + failedGroup := [{"Name": "Test Group 1", + "Value": NonComplianceMessage16_2}, + {"Name": "Test Group 2", + "Value": NonComplianceMessage16_2}] + FailTestGroupNonCompliant(PolicyId, Output, failedGroup) } test_EarlyAccessApps_OUs_Groups_Incorrect_V1 if { @@ -424,18 +377,13 @@ test_EarlyAccessApps_OUs_Groups_Incorrect_V1 if { } } - RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId] - count(RuleOutput) == 1 - not RuleOutput[0].RequirementMet - not RuleOutput[0].NoSuchEvent - RuleOutput[0].ReportDetails == concat("", [ - "The following OUs are non-compliant:
", - "The following groups are non-compliant:" - ]) + + failedGroup := [{"Name": "Test Group 1", + "Value": NonComplianceMessage16_2}, + {"Name": "Test Group 2", + "Value": NonComplianceMessage16_2}] + failedOU := [{"Name": "Test Top-Level OU", + "Value": NonComplianceMessage16_2}] + FailTestBothNonCompliant(PolicyId, Output, failedOU, failedGroup) } #-- diff --git a/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api01_test.rego b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api01_test.rego new file mode 100644 index 00000000..245c0be5 --- /dev/null +++ b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api01_test.rego @@ -0,0 +1,151 @@ +package commoncontrols + +import future.keywords +import data.utils +import data.utils.FailTestOUNonCompliant +import data.utils.PassTestResult + +GoodCaseInputApi01 := { + "policies": { + "topOU": { + "security_two_step_verification_device_trust": { + "allowTrustingDevice": false + }, + "security_two_step_verification_enforcement": { + "enforcedFrom": "2024-02-16T23:22:21.732Z" + }, + "security_two_step_verification_enforcement_factor": { + "allowedSignInFactorSet": "PASSKEY_ONLY" + }, + "security_two_step_verification_enrollment": { + "allowEnrollment": true + }, + "security_two_step_verification_grace_period": { + "enrollmentGracePeriod": "168h"} + }, + "nextOU": { + "security_two_step_verification_grace_period": { + "enrollmentGracePeriod": "604800s"} + } + }, + "tenant_info": { + "topLevelOU": "topOU" + } +} + +BadCaseInputApi01 := { + "policies": { + "topOU": { + "security_two_step_verification_device_trust": { + "allowTrustingDevice": true + }, + "security_two_step_verification_enforcement": { + "enforcedFrom": "2025-02-16T23:22:21.732Z" + }, + "security_two_step_verification_enforcement_factor": { + "allowedSignInFactorSet": "ALL" + }, + "security_two_step_verification_enrollment": { + "allowEnrollment": false + }, + "security_two_step_verification_grace_period": { + "enrollmentGracePeriod": "0s"} + }, + "nextOU": { + "security_two_step_verification_enforcement": { + "enforcedFrom": "2028-02-16T23:22:21.732Z" + }, + "security_two_step_verification_enforcement_factor": { + "allowedSignInFactorSet": "ALL" + }, + "security_two_step_verification_enrollment": { + "allowEnrollment": true + } + }, + "thirdOU": { + "security_two_step_verification_enforcement": { + "enforcedFrom": "2035-02-16T23:22:21.732Z" + }, + "security_two_step_verification_enforcement_factor": { + "allowedSignInFactorSet": "PASSKEY_ONLY" + }, + "security_two_step_verification_enrollment": { + "allowEnrollment": true + } + } + }, + "tenant_info": { + "topLevelOU": "topOU" + } +} + +BadCaseInputApi01a := { + "policies": { + "topOU": { + "security_login_challenges": { + "enableEmployeeIdChallenge": true + } + }, + "nextOU": { + "security_login_challenges": { + "enableEmployeeIdChallenge": false + } + } + }, + "tenant_info": { + "topLevelOU": "topOU" + } +} + +test_2SV_Correct_1 if { + PolicyId := CommonControlsId1_1 + Output := tests with input as GoodCaseInputApi01 + + PassTestResult(PolicyId, Output) +} + +test_2SV_Incorrect_1 if { + PolicyId := CommonControlsId1_1 + Output := tests with input as BadCaseInputApi01 + + failedOU := [{"Name": "nextOU", + "Value": NonComplianceMessage1_1b(GetFriendlyMethods("ALL"))}, + {"Name": "thirdOU", + "Value": NonComplianceMessage1_1c}, + {"Name": "topOU", + "Value": NonComplianceMessage1_1a}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) +} + +test_EnrollPeriod_Correct_1 if { + PolicyId := CommonControlsId1_2 + Output := tests with input as GoodCaseInputApi01 + + PassTestResult(PolicyId, Output) +} + +test_EnrollPeriod_Incorrect_1 if { + PolicyId := CommonControlsId1_2 + Output := tests with input as BadCaseInputApi01 + + failedOU := [{"Name": "topOU", + "Value": NonComplianceMessage1_2(0, + utils.DurationToSeconds("7d"))}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) +} + +test_DeviceTrust_Correct_1 if { + PolicyId := CommonControlsId1_3 + Output := tests with input as GoodCaseInputApi01 + + PassTestResult(PolicyId, Output) +} + +test_DeviceTrust_Incorrect_1 if { + PolicyId := CommonControlsId1_3 + Output := tests with input as BadCaseInputApi01 + + failedOU := [{"Name": "topOU", + "Value": NonComplianceMessage1_3}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) +} diff --git a/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api04_test.rego b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api04_test.rego index 39c7d038..6aa43b55 100644 --- a/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api04_test.rego +++ b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api04_test.rego @@ -29,7 +29,7 @@ GoodCaseInputApi04 := { } } -BaseCaseInputApi04 := { +BadCaseInputApi04 := { "policies": { "topOU": { "security_session_controls": { @@ -56,7 +56,7 @@ test_CCAPI_ReAuth_Comply_1 if { test_CCAPI_ReAuth_NonComply_1 if { PolicyId := CommonControlsId4_1 - Output := tests with input as BaseCaseInputApi04 + Output := tests with input as BadCaseInputApi04 failedOU := [{"Name": "nextOU", "Value": NonComplianceMessage4_1(GetFriendlyValue4_1(800 * 60))}] diff --git a/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api16_test.rego b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api16_test.rego new file mode 100644 index 00000000..c81248a7 --- /dev/null +++ b/scubagoggles/Testing/RegoTests/commoncontrols/commoncontrols_api16_test.rego @@ -0,0 +1,66 @@ +package commoncontrols + +import future.keywords +import data.utils.FailTestOUNonCompliant +import data.utils.PassTestResult + +GoodCaseInputApi16 := { + "policies": { + "topOU": { + "early_access_apps_service_status": {"serviceState": "DISABLED"} + } + }, + "tenant_info": { + "topLevelOU": "topOU" + } +} + +BadCaseInputApi16 := { + "policies": { + "topOU": { + "early_access_apps_service_status": {"serviceState": "ENABLED"} + } + }, + "tenant_info": { + "topLevelOU": "topOU" + } +} + +BadCaseInputApi16a := { + "policies": { + "topOU": { + "early_access_apps_service_status": {"serviceState": "DISABLED"} + }, + "nextOU": { + "early_access_apps_service_status": {"serviceState": "ENABLED"} + }, + }, + "tenant_info": { + "topLevelOU": "topOU" + } +} + +test_EarlyAccess_Correct_1 if { + PolicyId := CommonControlsId16_2 + Output := tests with input as GoodCaseInputApi16 + + PassTestResult(PolicyId, Output) +} + +test_EarlyAccess_Incorrect_1 if { + PolicyId := CommonControlsId16_2 + Output := tests with input as BadCaseInputApi16 + + failedOU := [{"Name": "topOU", + "Value": NonComplianceMessage16_2}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) +} + +test_EarlyAccess_Incorrect_2 if { + PolicyId := CommonControlsId16_2 + Output := tests with input as BadCaseInputApi16a + + failedOU := [{"Name": "nextOU", + "Value": NonComplianceMessage16_2}] + FailTestOUNonCompliant(PolicyId, Output, failedOU) +} diff --git a/scubagoggles/baselines/commoncontrols.md b/scubagoggles/baselines/commoncontrols.md index d3cd1d92..1ba842e9 100644 --- a/scubagoggles/baselines/commoncontrols.md +++ b/scubagoggles/baselines/commoncontrols.md @@ -1306,6 +1306,8 @@ A custom policy SHALL be configured for Gmail to protect PII and sensitive infor - [T1048:002: Exfiltration Over Alternative Protocol: Exfiltration Over Asymmetric Encrypted Non-C2 Protocol](https://attack.mitre.org/techniques/T1048/002/) - [T1213: Data from Information Repositories](https://attack.mitre.org/techniques/T1213/) +[//]: # (Keep the version suffix out of the anchor.) + #### GWS.COMMONCONTROLS.18.4v0.3 The action for the above DLP policies SHOULD be set to block external sharing. @@ -1347,7 +1349,7 @@ Drive DLP and Chat DLP are available to Cloud Identity Premium users with a Goog 2. Click **Add Condition**. For **Content type to scan** select **All content**. For **What to scan for** select **Matches predefined data type**. For **Select data type** select **United States - Individual Taxpayer Identification Number**. Select the remaining condition properties according to agency need. 3. Click **Add Condition**. For **Content type to scan** select **All content**. For **What to scan for** select **Matches predefined data type**. For **Select data type** select **United States - Social Security Number***. Select the remaining condition properties according to agency need. 4. Configure other appropriate content and condition definition(s) based upon the agency's individual requirements and click **Continue**. -5. In the **Actions** section, select **Block external sharing** (per [GWS.COMMONCONTROLS.18.4v0.3](#gwscommoncontrols184v03)). +5. In the **Actions** section, select **Block external sharing** (per [GWS.COMMONCONTROLS.18.4](#commoncontrols184)). 6. In the **Alerting** section, choose a severity level, and optionally, check **Send to alert center to trigger notifications**. 7. Review the rule details, mark the rule as **Active**, and click **Create.** @@ -1360,7 +1362,7 @@ Drive DLP and Chat DLP are available to Cloud Identity Premium users with a Goog 2. Click **Add Condition**. For **Content type to scan** select **All content**. For **What to scan for** select **Matches predefined data type**. For **Select data type** select **United States - Individual Taxpayer Identification Number**. Select the remaining condition properties according to agency need. 3. Click **Add Condition**. For **Content type to scan** select **All content**. For **What to scan for** select **Matches predefined data type**. For **Select data type** select **United States - Social Security Number***. Select the remaining condition properties according to agency need. 4. Configure other appropriate content and condition definition(s) based upon the agency's individual requirements and click **Continue**. -5. In the **Actions** section, select **Block**. Under **Select when this action should apply**, select **External Conversations**, **Spaces**, **Group chats**, and **1:1 chats** (See [GWS.COMMONCONTROLS.18.4v0.3](#gwscommoncontrols184v03)). +5. In the **Actions** section, select **Block**. Under **Select when this action should apply**, select **External Conversations**, **Spaces**, **Group chats**, and **1:1 chats** (See [GWS.COMMONCONTROLS.18.4](#commoncontrols184)). 6. In the **Alerting** section, choose a severity level, and optionally, check **Send to alert center to trigger notifications**. 7. Review the rule details, mark the rule as **Active**, and click **Create.** @@ -1373,7 +1375,7 @@ Drive DLP and Chat DLP are available to Cloud Identity Premium users with a Goog 2. Click **Add Condition**. For **Content type to scan** select **All content**. For **What to scan for** select **Matches predefined data type**. For **Select data type** select **United States - Individual Taxpayer Identification Number**. Select the remaining condition properties according to agency need. 3. Click **Add Condition**. For **Content type to scan** select **All content**. For **What to scan for** select **Matches predefined data type**. For **Select data type** select **United States - Social Security Number***. Select the remaining condition properties according to agency need. 4. Configure other appropriate content and condition definition(s) based upon the agency's individual requirements and click **Continue**. -5. In the **Actions** section, select **Block message**. Under **Select when this action should apply**, check **Messages sent to external recipients** (See [GWS.COMMONCONTROLS.18.4v0.3](#gwscommoncontrols184v03)). +5. In the **Actions** section, select **Block message**. Under **Select when this action should apply**, check **Messages sent to external recipients** (See [GWS.COMMONCONTROLS.18.4](#commoncontrols184)). 6. In the **Alerting** section, choose a severity level, and optionally, check **Send to alert center to trigger notifications**. 7. Review the rule details, mark the rule as **Active**, and click **Create.** diff --git a/scubagoggles/rego/Commoncontrols.rego b/scubagoggles/rego/Commoncontrols.rego index eb4f54d7..94c5b1a2 100644 --- a/scubagoggles/rego/Commoncontrols.rego +++ b/scubagoggles/rego/Commoncontrols.rego @@ -131,21 +131,36 @@ NoSuchEvent1_1 := true if { count(Events) == 0 } +Check1_1_OK if { + not PolicyApiInUse + not NoSuchEvent1_1 +} + +Check1_1_OK if {PolicyApiInUse} + GetFriendlyMethods(Value) := "Any" if { - Value == "ANY" + Value in {"ALL", "ANY"} } else := "Any except verification codes via text, phone call" if { Value == "NO_TELEPHONY" } else := "Only security key and allow security codes without remote access" if { - Value == "SECURITY_KEY_AND_IP_BOUND_SECURITY_CODE" + Value in {"PASSKEY_PLUS_IP_BOUND_SECURITY_CODE", "SECURITY_KEY_AND_IP_BOUND_SECURITY_CODE"} } else := "Only security key and allow security codes with remote access" if { - Value == "SECURITY_KEY_AND_SECURITY_CODE" + Value in {"PASSKEY_PLUS_SECURITY_CODE", "SECURITY_KEY_AND_SECURITY_CODE"} } else := Value +NonComplianceMessage1_1a := "Users cannot enable 2-step verification (2SV)." + +NonComplianceMessage1_1b(value) := sprintf("Allowed methods is set to %s", + [value]) + +NonComplianceMessage1_1c := "2-step verification (2SV) is not enforced." + NonCompliantOUs1_1 contains { "Name": OU, "Value": "Allow users to turn on 2-Step Verification is OFF" } if { + not PolicyApiInUse some OU in utils.OUsWithEvents Events := FilterEventsOU("ALLOW_STRONG_AUTHENTICATION", OU) # Ignore OUs without any events. We're already asserting that the @@ -161,6 +176,7 @@ NonCompliantOUs1_1 contains { "Value": "2-Step Verification Enforcement is OFF" } if { + not PolicyApiInUse some OU in utils.OUsWithEvents Events := FilterEventsOU("ENFORCE_STRONG_AUTHENTICATION", OU) # Ignore OUs without any events. We're already asserting that the @@ -173,9 +189,10 @@ if { NonCompliantOUs1_1 contains { "Name": OU, - "Value": concat("", ["Allowed methods is set to ", GetFriendlyMethods(LastEvent.NewValue)]) + "Value": NonComplianceMessage1_1b(GetFriendlyMethods(LastEvent.NewValue)) } if { + not PolicyApiInUse some OU in utils.OUsWithEvents Events := FilterEventsOU("CHANGE_ALLOWED_TWO_STEP_VERIFICATION_METHODS", OU) # Ignore OUs without any events. We're already asserting that the @@ -192,6 +209,7 @@ NonCompliantGroups1_1 contains { "Value": "Allow users to turn on 2-Step Verification is Off" } if { + not PolicyApiInUse some Group in utils.GroupsWithEvents Events := FilterEventsGroup("ALLOW_STRONG_AUTHENTICATION", Group) # Ignore Groups without any events. @@ -205,6 +223,7 @@ NonCompliantGroups1_1 contains { "Value": "2-Step Verification Enforcement is Off" } if { + not PolicyApiInUse some Group in utils.GroupsWithEvents Events := FilterEventsGroup("ENFORCE_STRONG_AUTHENTICATION", Group) # Ignore Groups without any events. @@ -215,9 +234,10 @@ if { NonCompliantGroups1_1 contains { "Name": Group, - "Value": concat("", ["Allowed methods is set to ", GetFriendlyMethods(LastEvent.NewValue)]) + "Value": NonComplianceMessage1_1b(GetFriendlyMethods(LastEvent.NewValue)) } if { + not PolicyApiInUse some Group in utils.GroupsWithEvents Events := FilterEventsGroup("CHANGE_ALLOWED_TWO_STEP_VERIFICATION_METHODS", Group) # Ignore Groups without any events. @@ -227,6 +247,51 @@ if { LastEvent.NewValue != "INHERIT_FROM_PARENT" } +# There are 3 items to check for this baseline. First, users must be allowed to +# enroll in 2SV. If they have been enrolled, then the passkey (aka security +# key) is the only allowed 2SV method. If the method is also OK, 2SV +# enforcement must be enabled, and this is determined by ensuring the date +# of enforcement is in the past (before today). + +NonCompliantOUs1_1 contains { + "Name": OU, + "Value": NonComplianceMessage1_1a +} +if { + some OU, settings in input.policies + enable2SV := settings.security_two_step_verification_enrollment.allowEnrollment + not enable2SV +} + +NonCompliantOUs1_1 contains { + "Name": OU, + "Value": NonComplianceMessage1_1b(GetFriendlyMethods(enforceMethod)) +} +if { + some OU, settings in input.policies + enable2SV := settings.security_two_step_verification_enrollment.allowEnrollment + enable2SV + enforceMethod := settings.security_two_step_verification_enforcement_factor.allowedSignInFactorSet + enforceMethod != "PASSKEY_ONLY" +} + +NonCompliantOUs1_1 contains { + "Name": OU, + "Value": NonComplianceMessage1_1c +} +if { + today := time.now_ns() + RFC3339 := "2006-01-02T15:04:05Z07:00" + some OU, settings in input.policies + enable2SV := settings.security_two_step_verification_enrollment.allowEnrollment + enable2SV + enforceMethod := settings.security_two_step_verification_enforcement_factor.allowedSignInFactorSet + enforceMethod == "PASSKEY_ONLY" + enforce2SV := settings.security_two_step_verification_enforcement.enforcedFrom + enforceValue := time.parse_ns(RFC3339, enforce2SV) + enforceValue > today +} + tests contains { "PolicyId": CommonControlsId1_1, "Criticality": "Shall", @@ -236,8 +301,9 @@ tests contains { "NoSuchEvent": true } if { + not PolicyApiInUse DefaultSafe := false - NoSuchEvent1_1 == true + not Check1_1_OK } tests contains { @@ -249,7 +315,7 @@ tests contains { "NoSuchEvent": false } if { - NoSuchEvent1_1 == false + Check1_1_OK Conditions := {count(NonCompliantOUs1_1) == 0, count(NonCompliantGroups1_1) == 0} Status := (false in Conditions) == false } @@ -261,13 +327,29 @@ if { CommonControlsId1_2 := utils.PolicyIdWithSuffix("GWS.COMMONCONTROLS.1.2") +LogMessage1_2 := "CHANGE_TWO_STEP_VERIFICATION_ENROLLMENT_PERIOD_DURATION" + +Check1_2_OK if { + not PolicyApiInUse + events := FilterEventsOU(LogMessage1_2, utils.TopLevelOU) + count(events) > 0 +} + +Check1_2_OK if {PolicyApiInUse} + +NonComplianceMessage1_2(value, expected) := sprintf("New user enrollment period (%ds) %s (%ds)", + [value, + "doesn't match expected", + expected]) + NonCompliantOUs1_2 contains { "Name": OU, "Value": concat("", ["New user enrollment period is set to ", LastEvent.NewValue]) } if { + not PolicyApiInUse some OU in utils.OUsWithEvents - Events := FilterEventsOU("CHANGE_TWO_STEP_VERIFICATION_ENROLLMENT_PERIOD_DURATION", OU) + Events := FilterEventsOU(LogMessage1_2, OU) # Ignore OUs without any events. We're already asserting that the # top-level OU has at least one event; for all other OUs we assume # they inherit from a parent OU if they have no events. @@ -282,8 +364,9 @@ NonCompliantGroups1_2 contains { "Value": concat("", ["New user enrollment period is set to ", LastEvent.NewValue]) } if { + not PolicyApiInUse some Group in utils.GroupsWithEvents - Events := FilterEventsGroup("CHANGE_TWO_STEP_VERIFICATION_ENROLLMENT_PERIOD_DURATION", Group) + Events := FilterEventsGroup(LogMessage1_2, Group) # Ignore groups without any events. count(Events) > 0 LastEvent := utils.GetLastEvent(Events) @@ -291,6 +374,18 @@ if { LastEvent.NewValue != "INHERIT_FROM_PARENT" } +NonCompliantOUs1_2 contains { + "Name": OU, + "Value": NonComplianceMessage1_2(enrollSeconds, expectedPeriod) +} +if { + expectedPeriod := utils.DurationToSeconds("7d") + some OU, settings in input.policies + enrollPeriod := settings.security_two_step_verification_grace_period.enrollmentGracePeriod + enrollSeconds := utils.DurationToSeconds(enrollPeriod) + enrollSeconds != expectedPeriod +} + tests contains { "PolicyId": CommonControlsId1_2, "Criticality": "Shall", @@ -300,9 +395,9 @@ tests contains { "NoSuchEvent": true } if { + not PolicyApiInUse DefaultSafe := false - Events := FilterEventsOU("CHANGE_TWO_STEP_VERIFICATION_ENROLLMENT_PERIOD_DURATION", utils.TopLevelOU) - count(Events) == 0 + not Check1_2_OK } tests contains { @@ -314,8 +409,7 @@ tests contains { "NoSuchEvent": false } if { - Events := FilterEventsOU("CHANGE_TWO_STEP_VERIFICATION_ENROLLMENT_PERIOD_DURATION", utils.TopLevelOU) - count(Events) > 0 + Check1_2_OK Conditions := {count(NonCompliantOUs1_2) == 0, count(NonCompliantGroups1_2) == 0} Status := (false in Conditions) == false } @@ -327,6 +421,18 @@ if { CommonControlsId1_3 := utils.PolicyIdWithSuffix("GWS.COMMONCONTROLS.1.3") +LogMessage1_3 := "CHANGE_TWO_STEP_VERIFICATION_FREQUENCY" + +Check1_3_OK if { + not PolicyApiInUse + events := FilterEventsOU(LogMessage1_3, utils.TopLevelOU) + count(events) > 0 +} + +Check1_3_OK if {PolicyApiInUse} + +NonComplianceMessage1_3 := "User is allowed to trust device." + GetFriendlyValue1_3(Value) := "ON" if { Value == "ENABLE_USERS_TO_TRUST_DEVICE" } else := Value @@ -336,6 +442,7 @@ NonCompliantOUs1_3 contains { "Value": concat("", ["Allow user to trust the device is ", GetFriendlyValue1_3(LastEvent.NewValue)]) } if { + not PolicyApiInUse some OU in utils.OUsWithEvents Events := FilterEventsOU("CHANGE_TWO_STEP_VERIFICATION_FREQUENCY", OU) # Ignore OUs without any events. We're already asserting that the @@ -352,8 +459,9 @@ NonCompliantGroups1_3 contains { "Value": concat("", ["Allow user to trust the device is ", GetFriendlyValue1_3(LastEvent.NewValue)]) } if { + not PolicyApiInUse some Group in utils.GroupsWithEvents - Events := FilterEventsGroup("CHANGE_TWO_STEP_VERIFICATION_FREQUENCY", Group) + Events := FilterEventsGroup(LogMessage1_3, Group) # Ignore groups without any events. count(Events) > 0 LastEvent := utils.GetLastEvent(Events) @@ -361,6 +469,16 @@ if { LastEvent.NewValue != "INHERIT_FROM_PARENT" } +NonCompliantOUs1_3 contains { + "Name": OU, + "Value": NonComplianceMessage1_3 +} +if { + some OU, settings in input.policies + trustDevice := settings.security_two_step_verification_device_trust.allowTrustingDevice + trustDevice +} + tests contains { "PolicyId": CommonControlsId1_3, "Criticality": "Shall", @@ -370,9 +488,9 @@ tests contains { "NoSuchEvent": true } if { + not PolicyApiInUse DefaultSafe := false - Events := FilterEventsOU("CHANGE_TWO_STEP_VERIFICATION_FREQUENCY", utils.TopLevelOU) - count(Events) == 0 + not Check1_3_OK } tests contains { @@ -384,8 +502,7 @@ tests contains { "NoSuchEvent": false } if { - Events := FilterEventsOU("CHANGE_TWO_STEP_VERIFICATION_FREQUENCY", utils.TopLevelOU) - count(Events) > 0 + Check1_3_OK Conditions := {count(NonCompliantOUs1_3) == 0, count(NonCompliantGroups1_3) == 0} Status := (false in Conditions) == false } @@ -657,19 +774,11 @@ NonCompliantOUs4_1 contains { "Value": NonComplianceMessage4_1(GetFriendlyValue4_1(durationSeconds)) } if { - multipliers := {"s": 1, "m": 60, "h": 3600} # This is the requirement limit for session duration: - webSessionMax := 12 * multipliers["h"] + webSessionMax := utils.DurationToSeconds("12h") some OU, settings in input.policies duration := settings.security_session_controls.webSessionDuration - result := regex.find_all_string_submatch_n(`(?i)^(\d+)([hms])$`, - duration, - 1) - firstMatch := result[0] - value := to_number(firstMatch[1]) - unit := firstMatch[2] - multiplier := multipliers[lower(unit)] - durationSeconds := value * multiplier + durationSeconds := utils.DurationToSeconds(duration) durationSeconds > webSessionMax } @@ -1113,11 +1222,7 @@ NonCompliantOUs5_6 contains { if { some OU, settings in input.policies passwordExpiration := settings.security_password.expirationDuration - result := regex.find_all_string_submatch_n(`(?i)^(\d+)[hms]$`, - passwordExpiration, - -1) - firstMatch := result[0] - expirationValue := to_number(firstMatch[1]) + expirationValue := utils.DurationToSeconds(passwordExpiration) expirationValue != 0 } @@ -2344,11 +2449,13 @@ if { CommonControlsId16_1 := utils.PolicyIdWithSuffix("GWS.COMMONCONTROLS.16.1") +NonComplianceMessage16_1 := "Access to additional services without individual control is turned on" + # NOTE: This setting cannot be controlled at the group level NonCompliantOUs16_1 contains { "Name": OU, - "Value": "Access to additional services without individual control is turned on" + "Value": NonComplianceMessage16_1 } if { some OU in utils.OUsWithEvents @@ -2412,11 +2519,26 @@ if { CommonControlsId16_2 := utils.PolicyIdWithSuffix("GWS.COMMONCONTROLS.16.2") +NonComplianceMessage16_2 := "Early access apps are ENABLED" + +Check16_2_OK if { + not PolicyApiInUse + Events := { + Event | some Event in ToggleServiceEvents; + Event.OrgUnit == utils.TopLevelOU; + Event.ServiceName == "Early Access Apps" + } + count(Events) > 0 +} + +Check16_2_OK if {PolicyApiInUse} + NonCompliantOUs16_2 contains { "Name": OU, - "Value": "Service status is ON" + "Value": NonComplianceMessage16_2 } if { + not PolicyApiInUse some OU in utils.OUsWithEvents # Note that this setting requires the custom ToggleServiceEvents rule. # Filter based on the service name of the event, otherwise all events are returned. @@ -2438,9 +2560,10 @@ if { NonCompliantGroups16_2 contains { "Name": Group, - "Value": "Service status is ON" + "Value": NonComplianceMessage16_2 } if { + not PolicyApiInUse some Group in utils.GroupsWithEvents # Note that this setting requires the custom ToggleServiceEvents rule. Events := { @@ -2454,6 +2577,16 @@ if { LastEvent.NewValue == "true" } +NonCompliantOUs16_2 contains { + "Name": OU, + "Value": NonComplianceMessage16_2 +} +if { + some OU, settings in input.policies + appsEnabled := utils.AppEnabled(input.policies, "early_access_apps", OU) + appsEnabled +} + tests contains { "PolicyId": CommonControlsId16_2, "Criticality": "Should", @@ -2463,14 +2596,9 @@ tests contains { "NoSuchEvent": true } if { + not PolicyApiInUse DefaultSafe := false - # Filter based on the service name of the event, otherwise all events are returned. - Events := { - Event | some Event in ToggleServiceEvents; - Event.OrgUnit == utils.TopLevelOU; - Event.ServiceName == "Early Access Apps" - } - count(Events) == 0 + not Check16_2_OK } tests contains { @@ -2482,14 +2610,7 @@ tests contains { "NoSuchEvent": false } if { - # This rule should execute only when log events exist - Events := { - Event | some Event in ToggleServiceEvents; - Event.OrgUnit == utils.TopLevelOU; - Event.ServiceName == "Early Access Apps" - } - count(Events) > 0 - + Check16_2_OK Conditions := { count(NonCompliantOUs16_2) == 0, count(NonCompliantGroups16_2) == 0 diff --git a/scubagoggles/rego/Utils.rego b/scubagoggles/rego/Utils.rego index 333a41b8..2696663b 100644 --- a/scubagoggles/rego/Utils.rego +++ b/scubagoggles/rego/Utils.rego @@ -564,3 +564,20 @@ GetFriendlyEnabledValue(Value) := "enabled" if { } else := "disabled" if { Value in {false, "false"} } else := Value + +# This function will convert a "duration string" (e.g., "18m" for 18 minutes) +# to an integer representing the time in seconds. This may be used for +# comparing string durations. Typically, Google's Policy API returns duration +# values in seconds. See CC 1.2 & CC 4.1 for examples of usage. + +DurationToSeconds(duration) := durationSeconds if { + multipliers := {"s": 1, "m": 60, "h": 3600, "d": 86400} + result := regex.find_all_string_submatch_n(`(?i)^(\d+)([dhms])$`, + duration, + 1) + firstMatch := result[0] + value := to_number(firstMatch[1]) + unit := firstMatch[2] + multiplier := multipliers[lower(unit)] + durationSeconds := value * multiplier +}