chore: revision

This commit is contained in:
Daniel Barranquero
2026-05-19 13:15:27 +02:00
parent 427e8e1600
commit 1ce6458c6e
7 changed files with 222 additions and 66 deletions
@@ -9,7 +9,7 @@
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "governance",
"Description": "Each Okta brand's sign-in page must present the Standard Mandatory DOD Notice and Consent Banner (DTM-08-060) before granting access (DISA STIG V-273192 / OKTA-APP-000200). The check inspects the customized sign-in page HTML for distinctive banner phrases. Brands without a customized sign-in page fall through to MANUAL: the default Okta page is not retrievable via the Management API.",
"Description": "Each Okta brand's sign-in page must present the Standard Mandatory DOD Notice and Consent Banner (DTM-08-060) before granting access (DISA STIG V-273192 / OKTA-APP-000200). The check inspects sign-in page HTML returned by the Okta Management API, using the customized page when present and otherwise falling back to the default sign-in page.",
"Risk": "Without the DOD Notice and Consent Banner, users are not informed that the system is a U.S. Government information system subject to monitoring. The absence of the banner weakens the legal basis for incident response and prosecution and breaks alignment with federal laws, Executive Orders, directives, policies, regulations, standards, and guidance.",
"RelatedUrl": "",
"AdditionalURLs": [
@@ -2,18 +2,31 @@ 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 SignInPage
# Distinctive substrings drawn from the DTM-08-060 Standard Mandatory DOD
# Notice and Consent Banner. The full banner is ~1300 characters and is
# highly variable across HTML encodings, so the check matches on a
# tolerant marker set: a customized sign-in page is considered to carry
# the banner when at least two of these markers appear (case-insensitive).
BANNER_MARKERS = (
"u.s. government",
"usg information system",
"only authorized use",
"may be intercepted",
# Distinctive marker groups drawn from the DTM-08-060 Standard Mandatory
# DOD Notice and Consent Banner. The HTML can vary across brands, so the
# check looks for the banner's core ideas rather than requiring an exact
# string match.
BANNER_MARKER_GROUPS = (
("u.s. government", "us government"),
("information system", "information systems"),
("authorized use only", "authorized use"),
(
"subject to monitoring",
"may be intercepted",
"searched, monitored, and recorded",
"consent to monitoring",
),
)
MIN_MARKER_MATCHES = 2
def _matched_banner_groups(content_lower: str) -> list[str]:
matched_markers: list[str] = []
for marker_group in BANNER_MARKER_GROUPS:
for marker in marker_group:
if marker in content_lower:
matched_markers.append(marker)
break
return matched_markers
class signon_dod_warning_banner_configured(Check):
@@ -21,9 +34,9 @@ class signon_dod_warning_banner_configured(Check):
Okta must display the Standard Mandatory DOD Notice and Consent
Banner (DTM-08-060) before granting access to the application. The
check inspects each brand's customized sign-in page; tenants without
a customized page fall through to MANUAL because the default Okta
page content is not retrievable via the Management API.
check inspects each brand's sign-in page HTML returned by the Okta
Management API, using the customized page when present and otherwise
falling back to the default sign-in page.
"""
def execute(self) -> list[CheckReportOkta]:
@@ -64,39 +77,41 @@ class signon_dod_warning_banner_configured(Check):
if page.fetch_error:
report.status = "MANUAL"
report.status_extended = (
f"Could not retrieve the customized sign-in page for "
f"Could not retrieve the sign-in page for "
f"brand '{page.brand_name or page.brand_id}' ({page.fetch_error}). "
"Inspect the brand customization manually to confirm the "
"Inspect the brand manually to confirm the "
"DOD Notice and Consent Banner (DTM-08-060) is displayed."
)
findings.append(report)
continue
if not page.is_customized or not page.page_content:
if not page.page_content:
report.status = "MANUAL"
report.status_extended = (
f"No customized sign-in page is configured for brand "
f"'{page.brand_name or page.brand_id}'. The DOD Notice "
"and Consent Banner cannot be audited via API — verify "
"the default sign-in page in the Admin Console."
f"Sign-in page content for brand "
f"'{page.brand_name or page.brand_id}' could not be "
"retrieved from the Okta API. Verify the DOD Notice and "
"Consent Banner (DTM-08-060) manually in the Admin Console."
)
findings.append(report)
continue
page_type = "customized" if page.is_customized else "default"
content_lower = page.page_content.lower()
matches = sum(1 for marker in BANNER_MARKERS if marker in content_lower)
matches = _matched_banner_groups(content_lower)
if matches >= MIN_MARKER_MATCHES:
if len(matches) == len(BANNER_MARKER_GROUPS):
report.status = "PASS"
report.status_extended = (
f"DOD Notice and Consent Banner detected on the customized "
f"DOD Notice and Consent Banner detected on the {page_type} "
f"sign-in page for brand '{page.brand_name or page.brand_id}' "
f"({matches} of {len(BANNER_MARKERS)} marker phrases matched)."
f"({len(matches)} of {len(BANNER_MARKER_GROUPS)} required "
"marker groups matched)."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Customized sign-in page for brand "
f"{page_type.title()} sign-in page for brand "
f"'{page.brand_name or page.brand_id}' does not contain "
"the DOD Notice and Consent Banner (DTM-08-060)."
)
@@ -93,6 +93,15 @@ class signon_global_session_lifetime_18h(Check):
)
return [report]
if lifetime == 0:
report.status = "FAIL"
report.status_extended = (
f"Priority 1 non-default rule '{priority_one_rule.name}' in "
f"Default Global Session Policy '{policy.name}' disables the "
"maximum Okta global session lifetime by setting it to 0 minutes."
)
return [report]
if lifetime <= threshold:
report.status = "PASS"
report.status_extended = (
@@ -36,10 +36,11 @@ class Signon(OktaService):
policy carries its rules; downstream checks read directly from this
structure.
Also populates `self.sign_in_pages` keyed by brand id with the
customized sign-in page HTML, used by the DOD warning-banner check.
Brands without a customized page are tracked with `is_customized=False`
so the check can render a MANUAL finding.
Also populates `self.sign_in_pages` keyed by brand id with sign-in page
HTML used by the DOD warning-banner check. When a brand has no
customized page, the service falls back to the default sign-in page
exposed by the Okta Management API and tracks it with
`is_customized=False`.
"""
def __init__(self, provider):
@@ -153,40 +154,51 @@ class Signon(OktaService):
for brand in all_brands:
brand_id = getattr(brand, "id", "") or ""
brand_name = getattr(brand, "name", "") or ""
page_result = await self.client.get_customized_sign_in_page(brand_id)
page_err = page_result[-1]
page_data = page_result[0]
if page_err is not None:
err_text = str(page_err).lower()
# 404 is the documented response when a brand has no
# customized sign-in page yet. The default Okta page still
# shows, but its content is not retrievable via API — flag
# the brand for MANUAL review downstream.
if (
"404" in err_text
or "not found" in err_text
or "e0000007" in err_text
):
result[brand_id] = SignInPage(
brand_id=brand_id,
brand_name=brand_name,
is_customized=False,
)
else:
result[brand_id] = SignInPage(
brand_id=brand_id,
brand_name=brand_name,
is_customized=False,
fetch_error=str(page_err),
)
continue
result[brand_id] = SignInPage(
result[brand_id] = await self._fetch_sign_in_page(brand_id, brand_name)
return result
async def _fetch_sign_in_page(self, brand_id: str, brand_name: str) -> "SignInPage":
page_result = await self.client.get_customized_sign_in_page(brand_id)
page_err = page_result[-1]
page_data = page_result[0]
if page_err is None:
return SignInPage(
brand_id=brand_id,
brand_name=brand_name,
is_customized=True,
page_content=getattr(page_data, "page_content", None),
)
return result
if not self._is_missing_customized_page_error(page_err):
return SignInPage(
brand_id=brand_id,
brand_name=brand_name,
is_customized=False,
fetch_error=str(page_err),
)
default_page_result = await self.client.get_default_sign_in_page(brand_id)
default_page_err = default_page_result[-1]
default_page_data = default_page_result[0]
if default_page_err is not None:
return SignInPage(
brand_id=brand_id,
brand_name=brand_name,
is_customized=False,
fetch_error=str(default_page_err),
)
return SignInPage(
brand_id=brand_id,
brand_name=brand_name,
is_customized=False,
page_content=getattr(default_page_data, "page_content", None),
)
@staticmethod
def _is_missing_customized_page_error(error) -> bool:
err_text = str(error).lower()
return "404" in err_text or "not found" in err_text or "e0000007" in err_text
@staticmethod
async def _paginate(fetch):
@@ -58,6 +58,7 @@ class Test_signon_dod_warning_banner_configured:
assert "DOD Notice and Consent Banner detected" in (
findings[0].status_extended
)
assert "customized sign-in page" in findings[0].status_extended
def test_fail_when_customized_page_missing_banner(self):
page = sign_in_page(
@@ -83,7 +84,31 @@ class Test_signon_dod_warning_banner_configured:
assert findings[0].status == "FAIL"
assert "does not contain" in findings[0].status_extended
def test_manual_when_no_customization(self):
def test_pass_when_default_page_contains_banner(self):
page = sign_in_page(
brand_id="brand-1",
brand_name="Primary",
is_customized=False,
page_content=f"<html><body>{DOD_BANNER_HTML_SNIPPET}</body></html>",
)
signon_client = build_signon_client(sign_in_pages={"brand-1": page})
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_dod_warning_banner_configured.signon_dod_warning_banner_configured import (
signon_dod_warning_banner_configured,
)
findings = signon_dod_warning_banner_configured().execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "default sign-in page" in findings[0].status_extended
def test_manual_when_page_content_missing(self):
page = sign_in_page(
brand_id="brand-1",
brand_name="Primary",
@@ -105,7 +130,9 @@ class Test_signon_dod_warning_banner_configured:
findings = signon_dod_warning_banner_configured().execute()
assert len(findings) == 1
assert findings[0].status == "MANUAL"
assert "No customized sign-in page" in findings[0].status_extended
assert "could not be retrieved from the Okta API" in (
findings[0].status_extended
)
def test_manual_when_fetch_error(self):
page = sign_in_page(
@@ -132,6 +159,33 @@ class Test_signon_dod_warning_banner_configured:
assert "Could not retrieve" in findings[0].status_extended
assert "403" in findings[0].status_extended
def test_fail_when_only_partial_banner_markers_are_present(self):
page = sign_in_page(
brand_id="brand-1",
brand_name="Primary",
is_customized=True,
page_content=(
"<html><body>This U.S. Government portal is for authorized use "
"only.</body></html>"
),
)
signon_client = build_signon_client(sign_in_pages={"brand-1": page})
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_dod_warning_banner_configured.signon_dod_warning_banner_configured import (
signon_dod_warning_banner_configured,
)
findings = signon_dod_warning_banner_configured().execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "does not contain" in findings[0].status_extended
def test_emits_one_finding_per_brand(self):
compliant = sign_in_page(
brand_id="brand-prod",
@@ -149,6 +203,7 @@ class Test_signon_dod_warning_banner_configured:
brand_id="brand-legacy",
brand_name="Legacy",
is_customized=False,
page_content=f"<html>{DOD_BANNER_HTML_SNIPPET}</html>",
)
signon_client = build_signon_client(
sign_in_pages={
@@ -174,5 +229,5 @@ class Test_signon_dod_warning_banner_configured:
assert by_brand == {
"brand-prod": "PASS",
"brand-sandbox": "FAIL",
"brand-legacy": "MANUAL",
"brand-legacy": "PASS",
}
@@ -120,6 +120,33 @@ class Test_signon_global_session_lifetime_18h:
assert findings[0].status == "FAIL"
assert "does not define" in findings[0].status_extended
def test_fail_when_lifetime_is_disabled_with_zero(self):
policy = default_policy(
[
non_default_rule("Unlimited Lifetime", lifetime_min=0, 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 "0 minutes" in findings[0].status_extended
assert "disables the maximum Okta global session lifetime" in (
findings[0].status_extended
)
def test_fail_when_default_rule_is_priority_one(self):
policy = default_policy(
[
@@ -192,6 +192,7 @@ class Test_Signon_service_brands:
provider,
brands_response,
sign_in_page_responses: dict,
default_sign_in_page_responses: dict | None = None,
):
async def fake_list_policies(*_a, **_k):
return ([], _resp({}), None)
@@ -202,6 +203,9 @@ class Test_Signon_service_brands:
async def fake_get_sign_in_page(brand_id, *_a, **_k):
return sign_in_page_responses[brand_id]
async def fake_get_default_sign_in_page(brand_id, *_a, **_k):
return default_sign_in_page_responses[brand_id]
with mock.patch(
"prowler.providers.okta.lib.service.service.OktaSDKClient"
) as mocked_client_cls:
@@ -209,6 +213,7 @@ class Test_Signon_service_brands:
mocked.list_policies = fake_list_policies
mocked.list_brands = fake_list_brands
mocked.get_customized_sign_in_page = fake_get_sign_in_page
mocked.get_default_sign_in_page = fake_get_default_sign_in_page
mocked_client_cls.return_value = mocked
return Signon(provider)
@@ -229,7 +234,27 @@ class Test_Signon_service_brands:
assert result.page_content == "<html>banner here</html>"
assert result.fetch_error is None
def test_404_marks_brand_as_not_customized(self):
def test_404_falls_back_to_default_sign_in_page(self):
provider = set_mocked_okta_provider()
brand = _fake_brand("brand-1", "Primary")
default_page = _fake_sign_in_page("<html>default banner here</html>")
service = self._build_with_brands(
provider,
brands_response=([brand], _resp({}), None),
sign_in_page_responses={
"brand-1": (None, _resp({}), Exception("404 Not Found"))
},
default_sign_in_page_responses={"brand-1": (default_page, _resp({}), None)},
)
assert service.sign_in_pages["brand-1"].is_customized is False
assert service.sign_in_pages["brand-1"].fetch_error is None
assert (
service.sign_in_pages["brand-1"].page_content
== "<html>default banner here</html>"
)
def test_default_sign_in_page_error_captured_when_customized_page_missing(self):
provider = set_mocked_okta_provider()
brand = _fake_brand("brand-1", "Primary")
service = self._build_with_brands(
@@ -238,10 +263,14 @@ class Test_Signon_service_brands:
sign_in_page_responses={
"brand-1": (None, _resp({}), Exception("404 Not Found"))
},
default_sign_in_page_responses={
"brand-1": (None, _resp({}), Exception("403 Forbidden"))
},
)
assert service.sign_in_pages["brand-1"].is_customized is False
assert service.sign_in_pages["brand-1"].fetch_error is None
result = service.sign_in_pages["brand-1"]
assert result.is_customized is False
assert "403" in result.fetch_error
def test_403_captured_into_fetch_error(self):
provider = set_mocked_okta_provider()
@@ -252,6 +281,7 @@ class Test_Signon_service_brands:
sign_in_page_responses={
"brand-1": (None, _resp({}), Exception("403 Forbidden: invalid_scope"))
},
default_sign_in_page_responses={},
)
result = service.sign_in_pages["brand-1"]
@@ -291,8 +321,16 @@ class Test_Signon_service_brands:
"brand-a": (page_a, _resp({}), None),
"brand-b": (None, _resp({}), Exception("404 not found")),
},
default_sign_in_page_responses={
"brand-b": (
_fake_sign_in_page("<html>default B</html>"),
_resp({}),
None,
)
},
)
assert set(service.sign_in_pages.keys()) == {"brand-a", "brand-b"}
assert service.sign_in_pages["brand-a"].page_content == "<html>A</html>"
assert service.sign_in_pages["brand-b"].is_customized is False
assert service.sign_in_pages["brand-b"].page_content == "<html>default B</html>"