mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-23 11:54:20 +00:00
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:
4 files changed
+395
No files matched your search
Whitespace-only changes.
+37
@@ -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": ""
|
||||
}
|
||||
+126
@@ -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=[],
|
||||
)
|
||||
+232
@@ -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
|
||||
Reference in new issue
Block a user