From 6187585919a705716a1ab76ecdb695867ecc5e79 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero Date: Tue, 19 May 2026 12:58:02 +0200 Subject: [PATCH] feat(sdk): add signon_global_session_cookies_not_persistent Okta check - Enforce DISA STIG V-273206 / OKTA-APP-001710 on the Default Global Session Policy - Require Priority 1 non-default rule that disables persistent session cookies - Treat unset use_persistent_cookie as non-compliant --- .../__init__.py | 0 ...ssion_cookies_not_persistent.metadata.json | 37 ++++ ...n_global_session_cookies_not_persistent.py | 119 ++++++++++ ...bal_session_cookies_not_persistent_test.py | 203 ++++++++++++++++++ 4 files changed, 359 insertions(+) create mode 100644 prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/__init__.py create mode 100644 prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.metadata.json create mode 100644 prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.py create mode 100644 tests/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent_test.py diff --git a/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/__init__.py b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.metadata.json b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.metadata.json new file mode 100644 index 0000000000..bc45b7dbea --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "signon_global_session_cookies_not_persistent", + "CheckTitle": "Default Global Session Policy has a Priority 1 non-default rule disabling persistent global session cookies", + "CheckType": [], + "ServiceName": "signon", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "The **Default Global Session Policy** must have a **Priority 1** rule that is **not** the built-in `Default Rule`, and that rule must set **Okta global session cookies persist across browser sessions** to `Disabled` (DISA STIG V-273206 / OKTA-APP-001710).", + "Risk": "Persistent global session cookies keep an authenticated Okta session alive across browser restarts. Cached authentication that survives the original browsing context can grant continued access long after a user closes the browser, and any cached authorization decisions outlive the session window the user expected.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-okta-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Okta Admin Console as a Super Admin\n2. Go to Security > Global Session Policy\n3. Open the Default Policy\n4. Add or edit a non-default rule\n5. Move that rule to Priority 1 so it is evaluated before the built-in Default Rule\n6. Set 'Okta global session cookies persist across browser sessions' to Disabled\n7. Save the rule", + "Terraform": "resource \"okta_policy_rule_signon\" \"prowler_no_persistent_cookies\" {\n policy_id = okta_policy_signon.default.id\n name = \"Prowler-enforced no persistent cookies\"\n status = \"ACTIVE\"\n session_persistent = false\n}\n" + }, + "Recommendation": { + "Text": "Configure the Default Global Session Policy so its Priority 1 non-default rule disables persistent global session cookies.", + "Url": "https://hub.prowler.com/check/signon_global_session_cookies_not_persistent" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.py b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.py new file mode 100644 index 0000000000..beab7311d3 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.py @@ -0,0 +1,119 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.signon.signon_client import signon_client +from prowler.providers.okta.services.signon.signon_service import GlobalSessionPolicy + + +class signon_global_session_cookies_not_persistent(Check): + """STIG V-273206 / OKTA-APP-001710. + + The DISA STIG requires the Okta Default Policy to have an active + Priority 1 rule that is not the built-in Default Rule, and that + rule must disable persistent global session cookies so the session + does not survive across browser restarts. + """ + + def execute(self) -> list[CheckReportOkta]: + org_domain = signon_client.provider.identity.org_domain + policy = self._get_default_policy() + report = CheckReportOkta( + metadata=self.metadata(), resource=policy, org_domain=org_domain + ) + + if policy.id == "default-policy-missing": + report.status = "FAIL" + report.status_extended = ( + "Default Global Session Policy was not found. STIG V-273206 " + "requires the Default Policy to contain an active Priority 1 " + "non-default rule that disables persistent global session cookies." + ) + return [report] + + if policy.status and policy.status.upper() != "ACTIVE": + report.status = "FAIL" + report.status_extended = ( + f"Default Global Session Policy '{policy.name}' is in " + f"status '{policy.status}'. STIG V-273206 requires an active " + "Default Policy with an active Priority 1 non-default rule." + ) + return [report] + + active_rules = sorted( + [ + rule + for rule in policy.rules + if not rule.status or rule.status.upper() == "ACTIVE" + ], + key=lambda rule: ( + rule.priority if rule.priority is not None else float("inf"), + rule.name, + ), + ) + if not active_rules: + report.status = "FAIL" + report.status_extended = ( + f"Default Global Session Policy '{policy.name}' has no active " + "rules. STIG V-273206 requires an active Priority 1 non-default " + "rule that disables persistent global session cookies." + ) + return [report] + + priority_one_rule = active_rules[0] + if priority_one_rule.priority != 1: + report.status = "FAIL" + report.status_extended = ( + f"Default Global Session Policy '{policy.name}' has no active " + f"Priority 1 rule. The first active rule is '{priority_one_rule.name}' " + f"at priority {priority_one_rule.priority}." + ) + return [report] + + if priority_one_rule.is_default or priority_one_rule.name == "Default Rule": + report.status = "FAIL" + report.status_extended = ( + f"Default Global Session Policy '{policy.name}' uses " + f"'{priority_one_rule.name}' as its active Priority 1 rule. " + "The STIG requires a non-default Priority 1 rule." + ) + return [report] + + use_persistent_cookie = priority_one_rule.use_persistent_cookie + if use_persistent_cookie is None: + report.status = "FAIL" + report.status_extended = ( + f"Priority 1 non-default rule '{priority_one_rule.name}' in " + f"Default Global Session Policy '{policy.name}' does not " + "assert the 'Okta global session cookies persist across browser " + "sessions' setting." + ) + return [report] + + if use_persistent_cookie is False: + report.status = "PASS" + report.status_extended = ( + f"Priority 1 non-default rule '{priority_one_rule.name}' in " + f"Default Global Session Policy '{policy.name}' disables " + "persistent global session cookies." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Priority 1 non-default rule '{priority_one_rule.name}' in " + f"Default Global Session Policy '{policy.name}' allows " + "persistent global session cookies, leaving the session " + "active across browser restarts." + ) + return [report] + + @staticmethod + def _get_default_policy() -> GlobalSessionPolicy: + for policy in signon_client.global_session_policies.values(): + if policy.is_default or policy.name == "Default Policy": + return policy + return GlobalSessionPolicy( + id="default-policy-missing", + name="Default Policy", + priority=1, + status="MISSING", + is_default=True, + rules=[], + ) diff --git a/tests/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent_test.py b/tests/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent_test.py new file mode 100644 index 0000000000..7d9d5eae7b --- /dev/null +++ b/tests/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent_test.py @@ -0,0 +1,203 @@ +from unittest import mock + +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.signon.signon_fixtures import ( + build_signon_client, + custom_policy, + default_policy, + default_rule, + non_default_rule, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.signon." + "signon_global_session_cookies_not_persistent." + "signon_global_session_cookies_not_persistent.signon_client" +) + + +class Test_signon_global_session_cookies_not_persistent: + def test_no_policies(self): + signon_client = build_signon_client({}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_cookies_not_persistent.signon_global_session_cookies_not_persistent import ( + signon_global_session_cookies_not_persistent, + ) + + findings = signon_global_session_cookies_not_persistent().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "was not found" in findings[0].status_extended + + def test_pass_when_priority_one_rule_disables_persistent_cookies(self): + policy = default_policy( + [ + non_default_rule( + "Non-persistent cookies", + use_persistent_cookie=False, + priority=1, + ), + default_rule(priority=2), + ] + ) + signon_client = build_signon_client({"pol-default": policy}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_cookies_not_persistent.signon_global_session_cookies_not_persistent import ( + signon_global_session_cookies_not_persistent, + ) + + findings = signon_global_session_cookies_not_persistent().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disables persistent global session cookies" in ( + findings[0].status_extended + ) + + def test_fail_when_priority_one_rule_uses_persistent_cookies(self): + policy = default_policy( + [ + non_default_rule( + "Persistent cookies enabled", + use_persistent_cookie=True, + priority=1, + ), + default_rule(priority=2), + ] + ) + signon_client = build_signon_client({"pol-default": policy}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_cookies_not_persistent.signon_global_session_cookies_not_persistent import ( + signon_global_session_cookies_not_persistent, + ) + + findings = signon_global_session_cookies_not_persistent().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "allows persistent global session cookies" in ( + findings[0].status_extended + ) + + def test_fail_when_priority_one_rule_does_not_assert_setting(self): + policy = default_policy( + [ + GlobalSessionPolicyRule( + id="rule-no-session", + name="No Session Block", + priority=1, + status="ACTIVE", + is_default=False, + use_persistent_cookie=None, + ), + default_rule(priority=2), + ] + ) + signon_client = build_signon_client({"pol-default": policy}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_cookies_not_persistent.signon_global_session_cookies_not_persistent import ( + signon_global_session_cookies_not_persistent, + ) + + findings = signon_global_session_cookies_not_persistent().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "does not" in findings[0].status_extended + assert "assert" in findings[0].status_extended + + def test_fail_when_default_policy_is_inactive(self): + policy = GlobalSessionPolicy( + id="pol-default", + name="Default Policy", + priority=99, + status="INACTIVE", + is_default=True, + rules=[ + non_default_rule( + "Compliant", + use_persistent_cookie=False, + priority=1, + ) + ], + ) + signon_client = build_signon_client({"pol-default": policy}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_cookies_not_persistent.signon_global_session_cookies_not_persistent import ( + signon_global_session_cookies_not_persistent, + ) + + findings = signon_global_session_cookies_not_persistent().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "status 'INACTIVE'" in findings[0].status_extended + + def test_ignores_other_custom_policies(self): + compliant_default = default_policy( + [ + non_default_rule( + "Non-persistent", + use_persistent_cookie=False, + priority=1, + ), + default_rule(priority=2), + ] + ) + permissive_custom = custom_policy( + [ + non_default_rule( + "Sticky admin", + use_persistent_cookie=True, + priority=1, + ) + ] + ) + signon_client = build_signon_client( + {"pol-custom": permissive_custom, "pol-default": compliant_default} + ) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_cookies_not_persistent.signon_global_session_cookies_not_persistent import ( + signon_global_session_cookies_not_persistent, + ) + + findings = signon_global_session_cookies_not_persistent().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert findings[0].resource_name == "Default Policy"