mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-14 08:01:59 +00:00
feat(sdk): add signon_global_session_policy_network_zone_enforced Okta check
- Enforce DISA STIG V-279691 / OKTA-APP-003242 on the Default Global Session Policy - Require at least one active non-default rule mapping User's IP to a Network Zone - Allow include or exclude zones to satisfy the Access Control Policy alignment
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"Provider": "okta",
|
||||
"CheckID": "signon_global_session_policy_network_zone_enforced",
|
||||
"CheckTitle": "Default Global Session Policy applies a Network Zone condition aligned with the Access Control Policy",
|
||||
"CheckType": [],
|
||||
"ServiceName": "signon",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "governance",
|
||||
"Description": "The **Default Global Session Policy** must use the **IF User's IP is** condition, mapped to a Network Zone, on at least one active non-default rule (DISA STIG V-279691 / OKTA-APP-003242). The condition lets Okta allow or deny access according to the organization's Access Control Policy rather than relying solely on credential possession.",
|
||||
"Risk": "When the Global Session Policy does not restrict access by Network Zone, every authenticated entity is granted session establishment regardless of source IP. Authorization that does not factor in network location lets stolen credentials or out-of-band sessions reach the Okta dashboard from any internet-routable address, undermining the organization's Access Control Policy.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://help.okta.com/oie/en-us/content/topics/security/network/network-zones.htm",
|
||||
"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 > Networks and define the Network Zones (allow / deny) that match the organization's Access Control Policy\n3. Go to Security > Global Session Policy\n4. Open the Default Policy\n5. Add or edit a non-default rule\n6. In Conditions, set 'IF User's IP is' to In zone (allow) or Not in zone (deny) and select the Network Zone\n7. Save the rule",
|
||||
"Terraform": "resource \"okta_policy_rule_signon\" \"prowler_network_zone\" {\n policy_id = okta_policy_signon.default.id\n name = \"Prowler-enforced Network Zone\"\n status = \"ACTIVE\"\n network_connection = \"ZONE\"\n network_includes = [okta_network_zone.allowed.id]\n}\n"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Add or update a non-default rule in the Default Global Session Policy so it maps the IF User's IP condition to a Network Zone that reflects the organization's Access Control Policy.",
|
||||
"Url": "https://hub.prowler.com/check/signon_global_session_policy_network_zone_enforced"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
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_policy_network_zone_enforced(Check):
|
||||
"""STIG V-279691 / OKTA-APP-003242.
|
||||
|
||||
The DISA STIG requires the Default Global Session Policy to use an
|
||||
"IF User's IP is" condition mapped to a Network Zone on at least one
|
||||
active non-default rule, so access can be allowed or denied per the
|
||||
organization's Access Control Policy.
|
||||
"""
|
||||
|
||||
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-279691 "
|
||||
"requires the Default Policy to apply an IP-based Network "
|
||||
"Zone condition on at least one active non-default rule."
|
||||
)
|
||||
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-279691 requires an active "
|
||||
"Default Policy with an IP-based Network Zone condition."
|
||||
)
|
||||
return [report]
|
||||
|
||||
rules_with_zones = [
|
||||
rule
|
||||
for rule in policy.rules
|
||||
if (not rule.status or rule.status.upper() == "ACTIVE")
|
||||
and not rule.is_default
|
||||
and rule.name != "Default Rule"
|
||||
and (rule.network_zones_include or rule.network_zones_exclude)
|
||||
]
|
||||
|
||||
if not rules_with_zones:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Default Global Session Policy '{policy.name}' has no active "
|
||||
"non-default rule mapping User's IP to a Network Zone. The "
|
||||
"policy cannot allow or deny access based on the organization's "
|
||||
"Access Control Policy."
|
||||
)
|
||||
return [report]
|
||||
|
||||
rule_names = ", ".join(f"'{rule.name}'" for rule in rules_with_zones)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Default Global Session Policy '{policy.name}' uses Network Zone "
|
||||
f"conditions on the following active non-default rule(s): {rule_names}."
|
||||
)
|
||||
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=[],
|
||||
)
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.okta.services.signon.signon_service import (
|
||||
GlobalSessionPolicy,
|
||||
)
|
||||
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_policy_network_zone_enforced."
|
||||
"signon_global_session_policy_network_zone_enforced.signon_client"
|
||||
)
|
||||
|
||||
|
||||
class Test_signon_global_session_policy_network_zone_enforced:
|
||||
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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "FAIL"
|
||||
assert "was not found" in findings[0].status_extended
|
||||
|
||||
def test_pass_when_active_rule_includes_zone(self):
|
||||
policy = default_policy(
|
||||
[
|
||||
non_default_rule(
|
||||
"Allow-from-VPN",
|
||||
network_zones_include=["zone-corp"],
|
||||
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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "PASS"
|
||||
assert "Allow-from-VPN" in findings[0].status_extended
|
||||
|
||||
def test_pass_when_active_rule_excludes_zone(self):
|
||||
policy = default_policy(
|
||||
[
|
||||
non_default_rule(
|
||||
"Block-blacklist",
|
||||
network_zones_exclude=["zone-blocked"],
|
||||
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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "PASS"
|
||||
assert "Block-blacklist" in findings[0].status_extended
|
||||
|
||||
def test_fail_when_no_rule_uses_network_zone(self):
|
||||
policy = default_policy(
|
||||
[
|
||||
non_default_rule("No-zones rule", 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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "FAIL"
|
||||
assert "no active non-default rule mapping" in findings[0].status_extended
|
||||
|
||||
def test_fail_when_only_default_rule_uses_zones(self):
|
||||
# The built-in Default Rule should not be counted as satisfying
|
||||
# the STIG even if it carries zone conditions — the STIG requires
|
||||
# an explicit non-default rule.
|
||||
policy = default_policy(
|
||||
[
|
||||
GlobalSessionPolicyRule_with_zones(),
|
||||
]
|
||||
)
|
||||
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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "FAIL"
|
||||
|
||||
def test_fail_when_inactive_rule_has_zones(self):
|
||||
policy = default_policy(
|
||||
[
|
||||
non_default_rule(
|
||||
"Inactive zone rule",
|
||||
network_zones_include=["zone-corp"],
|
||||
priority=1,
|
||||
status="INACTIVE",
|
||||
),
|
||||
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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "FAIL"
|
||||
|
||||
def test_fail_when_default_policy_inactive(self):
|
||||
policy = GlobalSessionPolicy(
|
||||
id="pol-default",
|
||||
name="Default Policy",
|
||||
priority=99,
|
||||
status="INACTIVE",
|
||||
is_default=True,
|
||||
rules=[
|
||||
non_default_rule(
|
||||
"Allow-corp",
|
||||
network_zones_include=["zone-corp"],
|
||||
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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "FAIL"
|
||||
assert "status 'INACTIVE'" in findings[0].status_extended
|
||||
|
||||
def test_ignores_zones_on_other_custom_policies(self):
|
||||
default_no_zones = default_policy(
|
||||
[
|
||||
non_default_rule("Plain rule", priority=1),
|
||||
default_rule(priority=2),
|
||||
]
|
||||
)
|
||||
custom_with_zones = custom_policy(
|
||||
[
|
||||
non_default_rule(
|
||||
"Custom-allow",
|
||||
network_zones_include=["zone-corp"],
|
||||
priority=1,
|
||||
)
|
||||
]
|
||||
)
|
||||
signon_client = build_signon_client(
|
||||
{"pol-default": default_no_zones, "pol-custom": custom_with_zones}
|
||||
)
|
||||
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_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import (
|
||||
signon_global_session_policy_network_zone_enforced,
|
||||
)
|
||||
|
||||
findings = signon_global_session_policy_network_zone_enforced().execute()
|
||||
assert len(findings) == 1
|
||||
assert findings[0].status == "FAIL"
|
||||
assert findings[0].resource_name == "Default Policy"
|
||||
|
||||
|
||||
def GlobalSessionPolicyRule_with_zones():
|
||||
# Built-in Default Rule that still carries zone conditions — the STIG
|
||||
# requires the rule to be non-default, so this scenario must FAIL.
|
||||
from prowler.providers.okta.services.signon.signon_service import (
|
||||
GlobalSessionPolicyRule,
|
||||
)
|
||||
|
||||
return GlobalSessionPolicyRule(
|
||||
id="rule-default-zoned",
|
||||
name="Default Rule",
|
||||
priority=1,
|
||||
status="ACTIVE",
|
||||
is_default=True,
|
||||
network_zones_include=["zone-corp"],
|
||||
)
|
||||
Reference in New Issue
Block a user