Files

678 lines
22 KiB
Python

import re
from types import SimpleNamespace
from unittest.mock import MagicMock
from prowler.lib.check.compliance_models import (
ComplianceFramework,
OutputsConfig,
ScoringConfig,
SplitByConfig,
TableConfig,
TableLabels,
UniversalComplianceRequirement,
)
from prowler.lib.outputs.compliance.universal.universal_table import (
_build_requirement_check_map,
_get_group_key,
get_universal_table,
)
def _make_finding(check_id, status="PASS", muted=False):
"""Create a mock finding for table tests."""
finding = SimpleNamespace()
finding.check_metadata = SimpleNamespace(CheckID=check_id)
finding.status = status
finding.muted = muted
return finding
def _strip_ansi(text):
return re.sub(r"\x1b\[[0-9;]*m", "", text)
def _make_framework(requirements, table_config, provider="AWS"):
return ComplianceFramework(
framework="TestFW",
name="Test Framework",
provider=provider,
version="1.0",
description="Test",
requirements=requirements,
outputs=OutputsConfig(table_config=table_config) if table_config else None,
)
class TestBuildRequirementCheckMap:
"""Test cases for building the requirement-to-check map of a framework."""
def test_basic(self):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a", "check_b"]},
),
UniversalComplianceRequirement(
id="1.2",
description="test2",
attributes={"Section": "IAM"},
checks={"aws": ["check_b", "check_c"]},
),
]
fw = _make_framework(reqs, TableConfig(group_by="Section"))
check_map = _build_requirement_check_map(fw)
assert "check_a" in check_map
assert len(check_map["check_b"]) == 2
assert "check_c" in check_map
def test_dict_checks_no_provider_filter(self):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a"], "azure": ["check_b"]},
),
]
fw = _make_framework(reqs, TableConfig(group_by="Section"))
check_map = _build_requirement_check_map(fw)
assert "check_a" in check_map
assert "check_b" in check_map
def test_dict_checks_filtered_by_provider(self):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a"], "azure": ["check_b"]},
),
]
fw = _make_framework(reqs, TableConfig(group_by="Section"))
check_map = _build_requirement_check_map(fw, provider="aws")
assert "check_a" in check_map
assert "check_b" not in check_map
def test_dict_checks_provider_not_present(self):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a"], "azure": ["check_b"]},
),
]
fw = _make_framework(reqs, TableConfig(group_by="Section"))
check_map = _build_requirement_check_map(fw, provider="gcp")
assert len(check_map) == 0
class TestGetGroupKey:
"""Test cases for resolving the group key of a requirement."""
def test_normal_field(self):
req = UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={},
)
assert _get_group_key(req, "Section") == ["IAM"]
def test_tactics(self):
req = UniversalComplianceRequirement(
id="T1190",
description="test",
attributes={},
checks={},
tactics=["Initial Access", "Execution"],
)
assert _get_group_key(req, "_Tactics") == ["Initial Access", "Execution"]
class TestGroupedMode:
"""Test cases for grouped-mode universal compliance table rendering."""
def test_grouped_rendering(self, capsys, tmp_path):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a"]},
),
UniversalComplianceRequirement(
id="2.1",
description="test2",
attributes={"Section": "Logging"},
checks={"aws": ["check_b"]},
),
]
tc = TableConfig(group_by="Section")
fw = _make_framework(reqs, tc)
findings = [
_make_finding("check_a", "PASS"),
_make_finding("check_b", "FAIL"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
assert "IAM" in captured.out
assert "Logging" in captured.out
assert "PASS" in captured.out
assert "FAIL" in captured.out
def test_grouped_multi_section_no_undercount(self, capsys, tmp_path):
"""A single check mapped to several sections must be counted in
every section it belongs to, not only the first one seen."""
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a", "check_b"]},
),
UniversalComplianceRequirement(
id="2.1",
description="test2",
attributes={"Section": "Logging"},
checks={"aws": ["check_a"]},
),
]
tc = TableConfig(group_by="Section")
fw = _make_framework(reqs, tc)
# check_a (FAIL) belongs to both IAM and Logging sections; check_b
# (PASS, IAM only) is added so the overview total reaches 2 and the
# results table is rendered.
findings = [
_make_finding("check_a", "FAIL"),
_make_finding("check_b", "PASS"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
plain = _strip_ansi(captured.out)
# Both the IAM and Logging rows must report FAIL(1). Before the fix the
# second section seen (Logging) was undercounted to FAIL(0) and rendered
# as PASS. Anchor each occurrence to its own table row so an unrelated
# "FAIL(1)" elsewhere cannot mask an undercount.
iam_row = [
line for line in plain.splitlines() if "IAM" in line and "FAIL(1)" in line
]
logging_row = [
line
for line in plain.splitlines()
if "Logging" in line and "FAIL(1)" in line
]
assert len(iam_row) == 1
assert len(logging_row) == 1
def test_grouped_multi_section_muted_not_undercounted(self, capsys, tmp_path):
"""A single MUTED finding mapped to several groups must be counted in
the per-group Muted column of every group it belongs to."""
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a", "check_b"]},
),
UniversalComplianceRequirement(
id="2.1",
description="test2",
attributes={"Section": "Logging"},
checks={"aws": ["check_a"]},
),
]
tc = TableConfig(group_by="Section")
fw = _make_framework(reqs, tc)
# check_a is MUTED and belongs to both IAM and Logging; check_b is a
# plain FAIL so the overview total reaches 2 and the table is rendered.
findings = [
_make_finding("check_a", "FAIL", muted=True),
_make_finding("check_b", "FAIL"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
plain = _strip_ansi(captured.out)
# The muted finding belongs to both sections, so both the IAM row and
# the Logging row must carry a Muted count of 1 in their last cell.
muted_one_rows = re.findall(r"│\s*1\s*│\s*$", plain, flags=re.MULTILINE)
assert len(muted_one_rows) == 2
class TestSplitMode:
"""Test cases for split-mode universal compliance table rendering."""
def test_split_rendering(self, capsys, tmp_path):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "Storage", "Profile": "Level 1"},
checks={"aws": ["check_a"]},
),
UniversalComplianceRequirement(
id="1.2",
description="test2",
attributes={"Section": "Storage", "Profile": "Level 2"},
checks={"aws": ["check_b"]},
),
]
tc = TableConfig(
group_by="Section",
split_by=SplitByConfig(field="Profile", values=["Level 1", "Level 2"]),
)
fw = _make_framework(reqs, tc)
findings = [
_make_finding("check_a", "PASS"),
_make_finding("check_b", "FAIL"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
assert "Storage" in captured.out
assert "Level 1" in captured.out
assert "Level 2" in captured.out
def test_split_muted_multi_section_not_undercounted(self, capsys, tmp_path):
"""In split mode a single MUTED finding mapped to several groups must
be counted in the Muted column of every group it belongs to."""
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "Storage", "Profile": "Level 1"},
checks={"aws": ["check_a", "check_b"]},
),
UniversalComplianceRequirement(
id="2.1",
description="test2",
attributes={"Section": "Logging", "Profile": "Level 1"},
checks={"aws": ["check_a"]},
),
]
tc = TableConfig(
group_by="Section",
split_by=SplitByConfig(field="Profile", values=["Level 1", "Level 2"]),
)
fw = _make_framework(reqs, tc)
# check_a is MUTED and belongs to both Storage and Logging; check_b is a
# plain FAIL so the table is rendered.
findings = [
_make_finding("check_a", "FAIL", muted=True),
_make_finding("check_b", "FAIL"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
plain = _strip_ansi(captured.out)
# Both section rows must carry a Muted count of 1 (last cell). Before the
# fix only the first group seen incremented Muted, leaving the other 0.
muted_one_rows = re.findall(r"│\s*1\s*│\s*$", plain, flags=re.MULTILINE)
assert len(muted_one_rows) == 2
def test_split_same_group_value_not_double_counted(self, capsys, tmp_path):
"""A single finding whose check maps to several requirements that share
the same group and split value must count once for that group/split,
not once per requirement (FAIL(1), never FAIL(2))."""
reqs = [
# check_a appears in two requirements, both Storage / Level 1.
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "Storage", "Profile": "Level 1"},
checks={"aws": ["check_a"]},
),
UniversalComplianceRequirement(
id="1.2",
description="test2",
attributes={"Section": "Storage", "Profile": "Level 1"},
checks={"aws": ["check_a"]},
),
# A second group so the table renders with more than one finding.
UniversalComplianceRequirement(
id="2.1",
description="test3",
attributes={"Section": "Logging", "Profile": "Level 1"},
checks={"aws": ["check_b"]},
),
]
tc = TableConfig(
group_by="Section",
split_by=SplitByConfig(field="Profile", values=["Level 1", "Level 2"]),
)
fw = _make_framework(reqs, tc)
findings = [
_make_finding("check_a", "FAIL"),
_make_finding("check_b", "PASS"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
plain = _strip_ansi(captured.out)
# The Storage row must show FAIL(1) for Level 1, never FAIL(2).
assert "FAIL(1)" in plain
assert "FAIL(2)" not in plain
class TestScoredMode:
"""Test cases for scored-mode universal compliance table rendering."""
def test_scored_rendering(self, capsys, tmp_path):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM", "LevelOfRisk": 5, "Weight": 100},
checks={"aws": ["check_a"]},
),
UniversalComplianceRequirement(
id="1.2",
description="test2",
attributes={"Section": "IAM", "LevelOfRisk": 3, "Weight": 50},
checks={"aws": ["check_b"]},
),
]
tc = TableConfig(
group_by="Section",
scoring=ScoringConfig(risk_field="LevelOfRisk", weight_field="Weight"),
)
fw = _make_framework(reqs, tc)
findings = [
_make_finding("check_a", "PASS"),
_make_finding("check_b", "FAIL"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
assert "IAM" in captured.out
assert "Score" in captured.out
assert "Threat Score" in captured.out
def test_scored_multi_section_fail_not_undercounted(self, capsys, tmp_path):
"""In scored mode a single FAIL finding mapped to several groups must
show FAIL(1) in every group it belongs to, not only the first one."""
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM", "LevelOfRisk": 5, "Weight": 100},
checks={"aws": ["check_a", "check_b"]},
),
UniversalComplianceRequirement(
id="2.1",
description="test2",
attributes={"Section": "Logging", "LevelOfRisk": 3, "Weight": 50},
checks={"aws": ["check_a"]},
),
]
tc = TableConfig(
group_by="Section",
scoring=ScoringConfig(risk_field="LevelOfRisk", weight_field="Weight"),
)
fw = _make_framework(reqs, tc)
# check_a (FAIL) belongs to both IAM and Logging; check_b (PASS, IAM
# only) raises the overview total to 2 so the table is rendered.
findings = [
_make_finding("check_a", "FAIL"),
_make_finding("check_b", "PASS"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
plain = _strip_ansi(captured.out)
iam_row = [
line for line in plain.splitlines() if "IAM" in line and "FAIL(1)" in line
]
logging_row = [
line
for line in plain.splitlines()
if "Logging" in line and "FAIL(1)" in line
]
assert len(iam_row) == 1
assert len(logging_row) == 1
class TestCustomLabels:
"""Test cases for custom-label universal compliance table rendering."""
def test_ens_spanish_labels(self, capsys, tmp_path):
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Marco": "operacional"},
checks={"aws": ["check_a"]},
),
UniversalComplianceRequirement(
id="1.2",
description="test2",
attributes={"Marco": "organizativo"},
checks={"aws": ["check_b"]},
),
]
tc = TableConfig(
group_by="Marco",
labels=TableLabels(
pass_label="CUMPLE",
fail_label="NO CUMPLE",
provider_header="Proveedor",
title="Estado de Cumplimiento",
),
)
fw = _make_framework(reqs, tc)
findings = [_make_finding("check_a", "PASS"), _make_finding("check_b", "FAIL")]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"test_fw",
"output",
str(tmp_path),
False,
framework=fw,
)
captured = capsys.readouterr()
assert "CUMPLE" in captured.out
assert "Estado de Cumplimiento" in captured.out
class TestMultiProviderDictChecks:
"""Test cases for multi-provider dict checks in the universal table."""
def test_only_aws_checks_matched(self, capsys, tmp_path):
"""With dict checks and provider='aws', only AWS checks match findings."""
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a"], "azure": ["check_b"]},
),
UniversalComplianceRequirement(
id="2.1",
description="test2",
attributes={"Section": "Logging"},
checks={"aws": ["check_c"], "gcp": ["check_d"]},
),
]
tc = TableConfig(group_by="Section")
fw = ComplianceFramework(
framework="MultiCloud",
name="Multi",
description="Test",
requirements=reqs,
outputs=OutputsConfig(table_config=tc),
)
findings = [
_make_finding("check_a", "PASS"),
_make_finding("check_b", "FAIL"), # Azure check, should be ignored
_make_finding("check_c", "PASS"),
]
bulk_metadata = {
"check_a": MagicMock(Compliance=[]),
"check_b": MagicMock(Compliance=[]),
"check_c": MagicMock(Compliance=[]),
}
get_universal_table(
findings,
bulk_metadata,
"multi_cloud",
"output",
str(tmp_path),
False,
framework=fw,
provider="aws",
)
captured = capsys.readouterr()
assert "IAM" in captured.out
assert "Logging" in captured.out
# check_b (azure) should not have been counted as FAIL for AWS
assert "PASS" in captured.out
class TestNoTableConfig:
"""Test cases for the universal table when no table config is present."""
def test_returns_early_without_table_config(self, capsys, tmp_path):
fw = ComplianceFramework(
framework="TestFW",
name="Test",
provider="AWS",
description="Test",
requirements=[],
)
get_universal_table([], {}, "test", "out", str(tmp_path), False, framework=fw)
captured = capsys.readouterr()
assert captured.out == ""
def test_returns_early_without_framework(self, capsys, tmp_path):
get_universal_table([], {}, "test", "out", str(tmp_path), False, framework=None)
captured = capsys.readouterr()
assert captured.out == ""