Files
prowler/tests/lib/scan/scan_exclusions_test.py

179 lines
6.8 KiB
Python

"""Coverage for ``Scan`` constructor exclusion semantics.
The Scan class is the single execution entry point used by both the CLI
and the API worker. Its exclusion validation must:
- Reject duplicates and unknown identifiers with actionable errors.
- Validate excluded checks against the FULL provider catalog so a global
configuration can exclude a valid check that is not part of a scoped
run (see the SDK acceptance criteria for scan-configuration exclusions).
- Refuse a configuration that would leave nothing to execute.
- Produce a deterministic, sorted final scope.
The catalog dependencies (``CheckMetadata.get_bulk``, ``Compliance.get_bulk``,
``list_services``, ``load_checks_to_execute``) are patched so tests stay
focused on the exclusion logic and avoid walking the provider package tree.
"""
from unittest.mock import MagicMock, patch
import pytest
from prowler.lib.scan.exceptions.exceptions import (
ScanInvalidCheckError,
ScanInvalidServiceError,
)
from prowler.lib.scan.scan import Scan
from tests.providers.aws.utils import set_mocked_aws_provider
# The provider catalog for these tests: three checks spread across two
# services (``accessanalyzer`` and ``s3``). Keeps assertions readable.
PROVIDER_CATALOG = {
"accessanalyzer_enabled",
"s3_bucket_encryption_enabled",
"s3_bucket_public_access",
}
PROVIDER_SERVICES = ["accessanalyzer", "s3"]
@pytest.fixture
def scan_provider():
provider = set_mocked_aws_provider()
metadata = MagicMock()
metadata.Categories = []
bulk = {check: metadata for check in PROVIDER_CATALOG}
with (
patch(
"prowler.lib.scan.scan.CheckMetadata.get_bulk",
return_value=bulk,
),
patch("prowler.lib.scan.scan.Compliance.get_bulk", return_value={}),
patch(
"prowler.lib.scan.scan.update_checks_metadata_with_compliance",
side_effect=lambda _compliance, checks: checks,
),
patch(
"prowler.lib.scan.scan.load_checks_to_execute",
side_effect=lambda **kwargs: set(kwargs["check_list"] or PROVIDER_CATALOG),
),
patch(
"prowler.lib.scan.scan.list_services",
return_value=PROVIDER_SERVICES,
),
):
yield provider
class Test_Exclusion_No_Ops:
def test_none_lists_are_no_ops(self, scan_provider):
scan = Scan(scan_provider, excluded_checks=None, excluded_services=None)
assert scan.checks_to_execute == sorted(PROVIDER_CATALOG)
def test_empty_lists_are_no_ops(self, scan_provider):
scan = Scan(scan_provider, excluded_checks=[], excluded_services=[])
assert scan.checks_to_execute == sorted(PROVIDER_CATALOG)
class Test_Excluded_Checks:
def test_valid_check_is_removed_from_the_scope(self, scan_provider):
scan = Scan(
scan_provider,
excluded_checks=["s3_bucket_public_access"],
)
assert scan.checks_to_execute == sorted(
PROVIDER_CATALOG - {"s3_bucket_public_access"}
)
def test_excluded_check_may_be_outside_the_selected_scope(self, scan_provider):
# ``s3_bucket_public_access`` is not in the explicitly selected
# ``checks`` list but is still a valid provider check, so the
# global exclusion must be accepted and be a no-op for this run.
scan = Scan(
scan_provider,
checks=["accessanalyzer_enabled"],
excluded_checks=["s3_bucket_public_access"],
)
assert scan.checks_to_execute == ["accessanalyzer_enabled"]
def test_unknown_check_is_rejected(self, scan_provider):
with pytest.raises(ScanInvalidCheckError):
Scan(scan_provider, excluded_checks=["not_a_real_check"])
def test_duplicate_checks_are_rejected(self, scan_provider):
with pytest.raises(ScanInvalidCheckError):
Scan(
scan_provider,
excluded_checks=[
"s3_bucket_public_access",
"s3_bucket_public_access",
],
)
class Test_Excluded_Services:
def test_service_exclusion_removes_every_check_in_the_service(self, scan_provider):
scan = Scan(scan_provider, excluded_services=["s3"])
assert scan.checks_to_execute == ["accessanalyzer_enabled"]
def test_unknown_service_is_rejected(self, scan_provider):
with pytest.raises(ScanInvalidServiceError):
Scan(scan_provider, excluded_services=["not_a_real_service"])
def test_duplicate_services_are_rejected(self, scan_provider):
with pytest.raises(ScanInvalidServiceError):
Scan(scan_provider, excluded_services=["s3", "s3"])
class Test_Combined_Exclusions:
def test_selected_checks_plus_excluded_checks_and_services(self, scan_provider):
scan = Scan(
scan_provider,
checks=["accessanalyzer_enabled", "s3_bucket_encryption_enabled"],
excluded_checks=["s3_bucket_public_access"],
excluded_services=["s3"],
)
# The explicit ``checks`` selection is narrowed by both the
# excluded_checks (drops nothing extra here) and excluded_services
# (drops every s3 check), leaving accessanalyzer alone.
assert scan.checks_to_execute == ["accessanalyzer_enabled"]
def test_result_is_sorted_and_deterministic(self, scan_provider):
scan = Scan(
scan_provider,
excluded_checks=["s3_bucket_public_access"],
)
assert scan.checks_to_execute == sorted(scan.checks_to_execute)
class Test_Empty_Final_Scope_Is_Rejected:
def test_excluding_every_service_is_rejected(self, scan_provider):
with pytest.raises(ScanInvalidCheckError):
Scan(scan_provider, excluded_services=PROVIDER_SERVICES)
def test_excluding_every_check_is_rejected(self, scan_provider):
with pytest.raises(ScanInvalidCheckError):
Scan(scan_provider, excluded_checks=sorted(PROVIDER_CATALOG))
class Test_Already_Empty_Scope_Does_Not_Blame_Exclusions:
"""When a positive filter (severity, categories, checks that resolve
to nothing) leaves the scope empty *before* exclusions run, the
exclusion pass must not falsely claim to be the cause. Otherwise the
real reason (empty selection) is masked by a misleading error."""
def test_empty_initial_scope_with_valid_exclusions_does_not_raise(
self, scan_provider
):
# Force ``load_checks_to_execute`` to return an empty scope while
# keeping the exclusion inputs valid against the provider catalog.
with patch(
"prowler.lib.scan.scan.load_checks_to_execute",
return_value=set(),
):
scan = Scan(
scan_provider,
excluded_checks=["s3_bucket_public_access"],
)
assert scan.checks_to_execute == []