Files
prowler/tests/lib/outputs/compliance/config_status_dispatch_coverage_test.py

169 lines
6.5 KiB
Python

"""End-to-end coverage: every shipped framework that declares ``ConfigRequirements``
must apply the config-status override through the *real* table dispatcher.
The companion ``config_status_renderer_coverage_test`` proves no renderer file
ignores the override. This test closes the other half of the gap that let
``okta_idaas_stig`` ship ConfigRequirements its renderers never applied: it walks
every per-provider compliance JSON that declares constraints, routes a synthetic
PASS finding through ``display_compliance_table`` exactly as a scan would, and
asserts the requirement is forced to FAIL when the scan's config is too loose.
It runs each framework twice — once with a config that *violates* the first
constraint and once with a config that *satisfies* it — and asserts the violating
run reports strictly more failures. Comparing the two runs is language-neutral
(only the parenthesised counts are read, never the localized PASS/FAIL labels) and
self-checking (a renderer that ignored the override would report equal counts).
Universal (multi-provider) frameworks render through a different path and are
covered by ``universal/universal_table_config_requirements_test.py``.
"""
import glob
import io
import json
import pathlib
import re
import tempfile
from contextlib import redirect_stdout
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.compliance.compliance import display_compliance_table
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[4]
_COMPLIANCE_DIR = _REPO_ROOT / "prowler" / "compliance"
# Per-provider JSONs live in a provider subdir; top-level files are universal.
_PROVIDER_JSONS = sorted(glob.glob(str(_COMPLIANCE_DIR / "*" / "*.json")))
def _first_constraint(data):
"""Return ``(check, config_key, operator, value)`` of the first declared
constraint, or ``None`` when the framework declares none."""
for requirement in data.get("Requirements", []):
constraints = requirement.get("ConfigRequirements")
if constraints:
c = constraints[0]
return c["Check"], c["ConfigKey"], c["Operator"], c["Value"]
return None
def _violating_value(operator, value):
"""A config value that breaks the constraint (forces the requirement FAIL)."""
if operator == "lte":
return value + 1
if operator == "gte":
return value - 1
if operator == "eq":
if isinstance(value, bool):
return not value
if isinstance(value, (int, float)):
return value + 1
return f"{value}__violates__"
if operator == "in":
return "__not_in_allowed_set__"
if operator == "subset":
return list(value) + ["__extra_not_allowed__"]
if operator == "superset":
return []
raise AssertionError(f"unhandled operator {operator}")
def _satisfying_value(operator, value):
"""A config value that satisfies the constraint (requirement keeps its status)."""
if operator in ("lte", "gte", "eq"):
return value
if operator == "in":
return value[0]
if operator == "subset":
return list(value)
if operator == "superset":
return list(value)
raise AssertionError(f"unhandled operator {operator}")
def _fail_count(findings, bulk, name, provider, applied_config):
"""Render the framework table with ``applied_config`` and return the FAIL
count from the overview, or ``None`` when the table renders nothing."""
def _not_implemented(*_a, **_k):
raise NotImplementedError
fake_provider = SimpleNamespace(
audit_config=applied_config,
type=provider,
display_compliance_table=_not_implemented,
)
buffer = io.StringIO()
with tempfile.TemporaryDirectory() as tmp:
with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=fake_provider,
):
with redirect_stdout(buffer):
display_compliance_table(findings, bulk, name, "output", tmp, False)
plain = re.sub(r"\x1b\[[0-9;]*m", "", buffer.getvalue())
# The overview's first parenthesised count is always the FAIL tally, in
# every renderer and every locale (only the label is translated).
counts = re.findall(r"\(\s*(\d+)\s*\)", plain)
return int(counts[0]) if counts else None
def _frameworks_with_constraints():
for path in _PROVIDER_JSONS:
with open(path, encoding="utf-8") as f:
data = json.load(f)
if _first_constraint(data):
name = pathlib.Path(path).stem
yield pytest.param(path, data, id=name)
@pytest.mark.parametrize("path, data", list(_frameworks_with_constraints()))
def test_framework_constraints_force_fail_through_dispatcher(path, data):
provider = pathlib.Path(path).parent.name
name = pathlib.Path(path).stem
check, config_key, operator, value = _first_constraint(data)
compliance = Compliance(**data)
def _finding():
return SimpleNamespace(
check_metadata=SimpleNamespace(CheckID=check),
check_id=check,
status="PASS",
muted=False,
)
# Two findings: the renderers only print the table when more than one
# finding maps to the framework.
findings = [_finding(), _finding()]
bulk = {check: SimpleNamespace(Compliance=[compliance])}
strict = _fail_count(
findings, bulk, name, provider, {config_key: _satisfying_value(operator, value)}
)
loose = _fail_count(
findings, bulk, name, provider, {config_key: _violating_value(operator, value)}
)
if strict is None and loose is None:
# The framework's renderer gates rendering on its name/version and does
# not paint a table for this framework id. There is no status to assert
# here; the renderer itself is still proven config-aware by
# config_status_renderer_coverage_test. Surfaced rather than silently
# passed so the skip is visible.
pytest.skip(f"{name}: renderer paints no table for this framework id")
assert strict == 0, (
f"{name}: PASS findings reported {strict} failures with a compliant "
"config; the control run should be clean."
)
assert loose and loose > 0, (
f"{name}: a PASS finding whose requirement maps {check} ran with "
f"{config_key} too loose for the constraint ({operator} {value}) was NOT "
"forced to FAIL. The framework declares ConfigRequirements its renderer "
"fails to apply — wire it through the config-status helpers."
)