mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-23 03:44:15 +00:00
163 lines
5.5 KiB
Python
163 lines
5.5 KiB
Python
import re
|
|
from types import SimpleNamespace
|
|
|
|
from prowler.lib.outputs.compliance.cis.cis import get_cis_table
|
|
|
|
|
|
def _strip_ansi(text):
|
|
return re.sub(r"\x1b\[[0-9;]*m", "", text)
|
|
|
|
|
|
def _make_finding(check_id, status="PASS", muted=False):
|
|
return SimpleNamespace(
|
|
check_metadata=SimpleNamespace(CheckID=check_id),
|
|
status=status,
|
|
muted=muted,
|
|
)
|
|
|
|
|
|
def _attr(section, profile="Level 1"):
|
|
return SimpleNamespace(Section=section, Profile=profile)
|
|
|
|
|
|
def _make_compliance(provider, attributes, version="1.4", framework="CIS"):
|
|
"""Build a per-check CIS compliance with the given (section, profile) attrs."""
|
|
return SimpleNamespace(
|
|
Framework=framework,
|
|
Version=version,
|
|
Provider=provider,
|
|
Requirements=[SimpleNamespace(Attributes=attributes)],
|
|
)
|
|
|
|
|
|
def _make_compliance_multi_req(provider, attributes, version="1.4", framework="CIS"):
|
|
"""Build a per-check CIS compliance where each attr is its own requirement,
|
|
simulating a check that appears in several requirements."""
|
|
return SimpleNamespace(
|
|
Framework=framework,
|
|
Version=version,
|
|
Provider=provider,
|
|
Requirements=[SimpleNamespace(Attributes=[attr]) for attr in attributes],
|
|
)
|
|
|
|
|
|
class TestCISTable:
|
|
"""Verify multi-section counting and provider-column attribution for the CIS compliance table."""
|
|
|
|
def test_muted_multi_section_not_undercounted(self, capsys, tmp_path):
|
|
"""A single MUTED finding mapped to several sections must increment the
|
|
per-section Muted column for every section, not only the first seen.
|
|
|
|
CIS counts FAIL/PASS through Level 1/Level 2 buckets, so only the Muted
|
|
per-section count was affected by the undercount bug.
|
|
"""
|
|
bulk_metadata = {
|
|
# check_a is muted and belongs to two sections at once.
|
|
"check_a": SimpleNamespace(
|
|
Compliance=[
|
|
_make_compliance("aws", [_attr("1 IAM"), _attr("2 Logging")])
|
|
]
|
|
),
|
|
# A real (non-muted) finding so the table is rendered.
|
|
"check_b": SimpleNamespace(
|
|
Compliance=[_make_compliance("aws", [_attr("1 IAM")])]
|
|
),
|
|
}
|
|
findings = [
|
|
_make_finding("check_a", "FAIL", muted=True),
|
|
_make_finding("check_b", "PASS"),
|
|
]
|
|
|
|
get_cis_table(
|
|
findings,
|
|
bulk_metadata,
|
|
"cis_1.4_aws",
|
|
"output",
|
|
str(tmp_path),
|
|
False,
|
|
)
|
|
|
|
captured = capsys.readouterr()
|
|
plain = _strip_ansi(captured.out)
|
|
# Both section rows must carry a Muted count of 1 in their last cell.
|
|
# Before the fix only the first section seen got incremented.
|
|
muted_one_rows = re.findall(r"│\s*1\s*│\s*$", plain, flags=re.MULTILINE)
|
|
assert len(muted_one_rows) == 2
|
|
|
|
def test_same_section_level_not_double_counted(self, capsys, tmp_path):
|
|
"""A single finding whose check maps to several requirements that share
|
|
the same section and profile must count once for that section/level,
|
|
not once per requirement (FAIL(1), never FAIL(2))."""
|
|
bulk_metadata = {
|
|
# check_a is a single FAIL mapped to two requirements, both in the
|
|
# same section "1 IAM" and the same profile "Level 1".
|
|
"check_a": SimpleNamespace(
|
|
Compliance=[
|
|
_make_compliance_multi_req("aws", [_attr("1 IAM"), _attr("1 IAM")])
|
|
]
|
|
),
|
|
# A second finding in another section so the table renders.
|
|
"check_b": SimpleNamespace(
|
|
Compliance=[_make_compliance("aws", [_attr("2 Logging")])]
|
|
),
|
|
}
|
|
findings = [
|
|
_make_finding("check_a", "FAIL"),
|
|
_make_finding("check_b", "PASS"),
|
|
]
|
|
|
|
get_cis_table(
|
|
findings,
|
|
bulk_metadata,
|
|
"cis_1.4_aws",
|
|
"output",
|
|
str(tmp_path),
|
|
False,
|
|
)
|
|
|
|
captured = capsys.readouterr()
|
|
plain = _strip_ansi(captured.out)
|
|
# The "1 IAM" row must show FAIL(1) for Level 1, never FAIL(2).
|
|
assert "FAIL(1)" in plain
|
|
assert "FAIL(2)" not in plain
|
|
|
|
def test_provider_column_not_leaked_from_other_framework(self, capsys, tmp_path):
|
|
"""The Provider column must come from the matched CIS compliance, not
|
|
from a different framework that trails it in the compliance list."""
|
|
bulk_metadata = {
|
|
"check_a": SimpleNamespace(
|
|
Compliance=[
|
|
_make_compliance("aws", [_attr("1 IAM")]),
|
|
_make_compliance(
|
|
"gcp", [_attr("Other")], framework="OtherFramework"
|
|
),
|
|
]
|
|
),
|
|
"check_b": SimpleNamespace(
|
|
Compliance=[
|
|
_make_compliance("aws", [_attr("1 IAM")]),
|
|
_make_compliance(
|
|
"gcp", [_attr("Other")], framework="OtherFramework"
|
|
),
|
|
]
|
|
),
|
|
}
|
|
findings = [
|
|
_make_finding("check_a", "FAIL"),
|
|
_make_finding("check_b", "PASS"),
|
|
]
|
|
|
|
get_cis_table(
|
|
findings,
|
|
bulk_metadata,
|
|
"cis_1.4_aws",
|
|
"output",
|
|
str(tmp_path),
|
|
False,
|
|
)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "aws" in captured.out
|
|
# The trailing unrelated framework's provider must not leak in.
|
|
assert "gcp" not in captured.out
|