feat(sdk): add signon_global_session_lifetime_18h Okta check

- Enforce DISA STIG V-273203 / OKTA-APP-001665 on the Default Global Session Policy
- Require Priority 1 non-default rule with max session lifetime <= 18 hours
- Threshold overridable via okta_max_session_lifetime_minutes audit config key
This commit is contained in:
Daniel Barranquero committed 2026-05-19 12:57:55 +02:00
1 parent c73231ec95
commit 42b19038e0
4 files changed
+395

No files matched your search

@@ -0,0 +1,37 @@
{
"Provider": "okta",
"CheckID": "signon_global_session_lifetime_18h",
"CheckTitle": "Default Global Session Policy has a Priority 1 non-default rule limiting session lifetime to 18 hours",
"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 **Maximum Okta global session lifetime** to 18 hours or less (DISA STIG V-273203 / OKTA-APP-001665). The threshold defaults to 1080 minutes and is overridable via the `okta_max_session_lifetime_minutes` key in the audit config.",
"Risk": "Without an enforced session lifetime, an authenticated Okta session can be reused indefinitely without reauthentication. Stolen session material continues to grant access long after the original sign-in event, and authorization changes (role revocation, group removal) take effect only on the next forced reauth.",
"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 'Maximum Okta global session lifetime' to 18 hours or less\n7. Save the rule",
"Terraform": "resource \"okta_policy_rule_signon\" \"prowler_session_lifetime_18h\" {\n policy_id = okta_policy_signon.default.id\n name = \"Prowler-enforced session lifetime\"\n status = \"ACTIVE\"\n session_lifetime = 1080\n session_persistent = false\n}\n"
},
"Recommendation": {
"Text": "Configure the Default Global Session Policy so its Priority 1 non-default rule sets the Maximum Okta global session lifetime to 18 hours or less.",
"Url": "https://hub.prowler.com/check/signon_global_session_lifetime_18h"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,126 @@
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
DEFAULT_THRESHOLD_MINUTES = 18 * 60
class signon_global_session_lifetime_18h(Check):
"""STIG V-273203 / OKTA-APP-001665.
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 set the maximum Okta global session lifetime to the
configured threshold or lower (defaults to 18 hours per STIG;
override via `okta_max_session_lifetime_minutes` in the audit config).
"""
def execute(self) -> list[CheckReportOkta]:
audit_config = signon_client.audit_config or {}
threshold = audit_config.get(
"okta_max_session_lifetime_minutes", DEFAULT_THRESHOLD_MINUTES
)
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-273203 "
"requires the Default Policy to contain an active Priority 1 "
f"non-default rule with session lifetime <= {threshold} minutes."
)
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-273203 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-273203 requires an active Priority 1 non-default "
f"rule with session lifetime <= {threshold} minutes."
)
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]
lifetime = priority_one_rule.max_session_lifetime_minutes
if lifetime 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 define "
"a maximum Okta global session lifetime."
)
return [report]
if lifetime <= threshold:
report.status = "PASS"
report.status_extended = (
f"Priority 1 non-default rule '{priority_one_rule.name}' in "
f"Default Global Session Policy '{policy.name}' sets the "
f"maximum Okta global session lifetime to {lifetime} minutes, "
f"meeting the configured threshold of {threshold} minutes."
)
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}' sets the "
f"maximum Okta global session lifetime to {lifetime} minutes, "
f"exceeding the configured threshold of {threshold} minutes."
)
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=[],
)
@@ -0,0 +1,232 @@
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_lifetime_18h."
"signon_global_session_lifetime_18h.signon_client"
)
class Test_signon_global_session_lifetime_18h:
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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().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_non_default_rule_is_compliant(self):
policy = default_policy(
[
non_default_rule("18h rule", lifetime_min=1080, 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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "18h rule" in findings[0].status_extended
assert "1080 minutes" in findings[0].status_extended
def test_fail_when_lifetime_exceeds_threshold(self):
policy = default_policy(
[
non_default_rule("Loose 24h rule", lifetime_min=1440, 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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "1440 minutes" in findings[0].status_extended
assert "exceeding the configured threshold" in findings[0].status_extended
def test_fail_when_priority_one_rule_has_no_lifetime(self):
policy = default_policy(
[
GlobalSessionPolicyRule(
id="rule-no-session",
name="No Session Block",
priority=1,
status="ACTIVE",
is_default=False,
max_session_lifetime_minutes=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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "does not define" in findings[0].status_extended
def test_fail_when_default_rule_is_priority_one(self):
policy = default_policy(
[
default_rule(priority=1, lifetime_min=1080),
non_default_rule("Compliant", lifetime_min=1080, 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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "uses 'Default Rule' as its active Priority 1 rule" 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("18h rule", lifetime_min=1080, 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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().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("18h rule", lifetime_min=1080, priority=1),
default_rule(priority=2),
]
)
loose_custom = custom_policy(
[
non_default_rule("Long-lived admin", lifetime_min=4320, priority=1),
default_rule(priority=2),
]
)
signon_client = build_signon_client(
{"pol-custom": loose_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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert findings[0].resource_name == "Default Policy"
def test_threshold_overridden_via_audit_config(self):
policy = default_policy(
[
non_default_rule("Relaxed 24h", lifetime_min=1440, priority=1),
default_rule(priority=2),
]
)
signon_client = build_signon_client(
{"pol-default": policy},
audit_config={"okta_max_session_lifetime_minutes": 1440},
)
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_lifetime_18h.signon_global_session_lifetime_18h import (
signon_global_session_lifetime_18h,
)
findings = signon_global_session_lifetime_18h().execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "threshold of 1440 minutes" in findings[0].status_extended