Merge remote-tracking branch 'origin/PROWLER-1778-dynamic-output-report-generation' into PROWLER-1799-sdk-only-provider-property

This commit is contained in:
StylusFrost committed 2026-06-03 13:14:24 +02:00
commit a5702e2469
80 files changed
+4818 -38398

No files matched your search

@@ -94,21 +94,6 @@ class TestDispatchStartswith:
display_compliance_table(compliance_framework=framework_name, **_COMMON)
mock_fn.assert_called_once()
@pytest.mark.parametrize(
"framework_name",
[
"csa_ccm_4.0_aws",
"csa_ccm_4.0_azure",
"csa_ccm_4.0_gcp",
"csa_ccm_4.0_oraclecloud",
"csa_ccm_4.0_alibabacloud",
],
)
@patch(f"{MODULE}.get_csa_table")
def test_csa_dispatch(self, mock_fn, framework_name):
display_compliance_table(compliance_framework=framework_name, **_COMMON)
mock_fn.assert_called_once()
@pytest.mark.parametrize(
"framework_name",
["c5_aws", "c5_azure", "c5_gcp"],
@@ -12,6 +12,7 @@ Also validates that print_compliance_frameworks and print_compliance_requirement
work with universal ComplianceFramework objects (dict checks, None provider).
"""
import csv
import json
import os
from datetime import datetime, timezone
@@ -124,6 +125,41 @@ def _make_universal_framework(name="TestFW", version="1.0", with_table_config=Tr
)
def _make_framework_with_manual(name="MixedFW", version="1.0"):
"""Framework with one aws-covered requirement and one manual one.
The manual requirement has no aws checks, so for provider ``aws`` it is
emitted as a manual row/event — used to assert manual requirements are
not duplicated when the writer is reused across streaming batches.
"""
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="Covered requirement",
attributes={"Section": "IAM"},
checks={"aws": ["check_a"]},
),
UniversalComplianceRequirement(
id="2.1",
description="Manual requirement",
attributes={"Section": "GOV"},
checks={"aws": []},
),
]
metadata = [AttributeMetadata(key="Section", type="str")]
outputs = OutputsConfig(table_config=TableConfig(group_by="Section"))
return ComplianceFramework(
framework=name,
name=f"{name} Framework",
provider="AWS",
version=version,
description="Test framework",
requirements=reqs,
attributes_metadata=metadata,
outputs=outputs,
)
# ── Tests ────────────────────────────────────────────────────────────
@@ -728,3 +764,243 @@ class TestIdempotency:
# FW1 writer instances unchanged
assert second_writers[0] is first_writers[0]
assert second_writers[1] is first_writers[1]
class TestStreamingBatches:
"""Streaming-aware behaviour: ``from_cli`` / ``is_last`` / ``_flush``.
Regression coverage for the API streaming path where the helper is
invoked once per finding batch: before the fix only the first batch
was written (batches 2..N silently dropped) and manual requirements
were re-emitted on every batch.
"""
def _run_batches(self, tmp_path, fw, key, batches):
"""Invoke the helper once per (findings, is_last) batch, sharing
``generated_outputs`` so writers are reused like the API does."""
generated = {"compliance": []}
for findings, is_last in batches:
process_universal_compliance_frameworks(
input_compliance_frameworks={key},
universal_frameworks={key: fw},
finding_outputs=findings,
output_directory=str(tmp_path),
output_filename="out",
provider="aws",
generated_outputs=generated,
from_cli=False,
is_last=is_last,
)
return generated
def test_defaults_preserve_cli_single_call(self, tmp_path):
"""Defaults (``from_cli=True``, ``is_last=True``): a single call
still finalizes a valid, closed OCSF JSON array (CLI unchanged)."""
fw = _make_universal_framework()
generated = {"compliance": []}
process_universal_compliance_frameworks(
input_compliance_frameworks={"test_fw_1.0"},
universal_frameworks={"test_fw_1.0": fw},
finding_outputs=[_make_finding("check_a")],
output_directory=str(tmp_path),
output_filename="out",
provider="aws",
generated_outputs=generated,
)
ocsf_path = tmp_path / "compliance" / "out_test_fw_1.0.ocsf.json"
data = json.loads(ocsf_path.read_text())
assert isinstance(data, list) and len(data) >= 1
def test_multibatch_csv_keeps_every_batch(self, tmp_path):
"""Findings from batches 2..N must not be dropped (the bug)."""
fw = _make_universal_framework()
f1 = _make_finding("check_a", status="PASS")
f2 = _make_finding("check_a", status="FAIL")
generated = self._run_batches(
tmp_path, fw, "fw_1.0", [([f1], False), ([f2], True)]
)
content = (tmp_path / "compliance" / "out_fw_1.0.csv").read_text()
assert "check_a is PASS" in content # batch 1
assert "check_a is FAIL" in content # batch 2 — regression
# writer reused, not recreated: still just 1 CSV + 1 OCSF
assert len(generated["compliance"]) == 2
def test_multibatch_ocsf_valid_array_with_every_batch(self, tmp_path):
"""OCSF is a valid (closed) JSON array holding every batch's
events only after the ``is_last=True`` call."""
fw = _make_universal_framework()
f1 = _make_finding("check_a", status="PASS")
f2 = _make_finding("check_a", status="FAIL")
self._run_batches(tmp_path, fw, "fw_1.0", [([f1], False), ([f2], True)])
data = json.loads(
(tmp_path / "compliance" / "out_fw_1.0.ocsf.json").read_text()
)
assert isinstance(data, list)
assert len(data) >= 2 # one event per batch finding
def test_manual_requirement_not_duplicated_across_batches(self, tmp_path):
"""Manual requirement is emitted once (first batch, via __init__),
never re-emitted when the writer is reused (``include_manual=False``)."""
fw = _make_framework_with_manual()
f1 = _make_finding("check_a", status="PASS")
f2 = _make_finding("check_a", status="FAIL")
self._run_batches(tmp_path, fw, "fw_1.0", [([f1], False), ([f2], True)])
rows = list(
csv.DictReader(
(tmp_path / "compliance" / "out_fw_1.0.csv").read_text().splitlines(),
delimiter=";",
)
)
manual_rows = [r for r in rows if r["STATUS"] == "MANUAL"]
assert len(manual_rows) == 1
assert manual_rows[0]["REQUIREMENTS_ID"] == "2.1"
ocsf = json.loads(
(tmp_path / "compliance" / "out_fw_1.0.ocsf.json").read_text()
)
manual_events = [
e
for e in ocsf
if (e.get("compliance") or {}).get("requirements") == ["2.1"]
]
assert len(manual_events) == 1
def test_writer_reused_not_recreated_across_batches(self, tmp_path):
"""Three batches still yield exactly one CSV + one OCSF writer,
and the same instances are reused throughout."""
fw = _make_universal_framework()
generated = self._run_batches(
tmp_path,
fw,
"fw_1.0",
[
([_make_finding("check_a")], False),
([_make_finding("check_a")], False),
([_make_finding("check_a")], True),
],
)
assert len(generated["compliance"]) == 2
assert isinstance(generated["compliance"][0], UniversalComplianceOutput)
assert isinstance(generated["compliance"][1], OCSFComplianceOutput)
def test_label_without_version_still_outputs(self, tmp_path):
"""Empty framework version → label is the framework name only;
the helper still produces both artifacts without error."""
fw = _make_universal_framework(version="")
generated = {"compliance": []}
processed = process_universal_compliance_frameworks(
input_compliance_frameworks={"fw"},
universal_frameworks={"fw": fw},
finding_outputs=[_make_finding("check_a")],
output_directory=str(tmp_path),
output_filename="out",
provider="aws",
generated_outputs=generated,
from_cli=False,
is_last=True,
)
assert processed == {"fw"}
assert len(generated["compliance"]) == 2
assert (tmp_path / "compliance" / "out_fw.csv").exists()
assert (tmp_path / "compliance" / "out_fw.ocsf.json").exists()
def _csa_like_framework() -> ComplianceFramework:
"""Build a CSA CCM-style universal framework with checks across providers."""
requirement = UniversalComplianceRequirement(
id="A&A-01",
description="Audit and Assurance",
attributes={"Section": "Audit"},
checks={
"aws": ["aws_check"],
"azure": ["azure_check"],
"gcp": ["gcp_check"],
},
)
return ComplianceFramework(
framework="CSA_CCM",
name="CSA Cloud Controls Matrix",
version="4.0",
description="Multi-provider framework",
requirements=[requirement],
attributes_metadata=[AttributeMetadata(key="Section", type="str")],
outputs=OutputsConfig(table_config=TableConfig(group_by="Section")),
)
class TestMultiProviderUniversalFramework:
"""A top-level CSA-CCM-style framework produces a CSV+OCSF pair scoped
to the provider it is invoked with."""
@pytest.mark.parametrize(
"provider,check_id",
[
("aws", "aws_check"),
("azure", "azure_check"),
("gcp", "gcp_check"),
],
)
def test_per_provider_outputs_isolated(self, tmp_path, provider, check_id):
framework = _csa_like_framework()
generated = {"compliance": []}
process_universal_compliance_frameworks(
input_compliance_frameworks={"csa_ccm_4.0"},
universal_frameworks={"csa_ccm_4.0": framework},
finding_outputs=[_make_finding(check_id, provider=provider)],
output_directory=str(tmp_path),
output_filename="prowler_output",
provider=provider,
generated_outputs=generated,
)
ocsf_path = tmp_path / "compliance" / "prowler_output_csa_ccm_4.0.ocsf.json"
events = json.loads(ocsf_path.read_text())
assert isinstance(events, list)
non_manual = [event for event in events if event.get("status_code") != "MANUAL"]
assert len(non_manual) == 1
assert non_manual[0]["compliance"]["checks"][0]["uid"] == check_id
class TestMitreStyleOCSFOutput:
"""MITRE attrs wrapped as `{"_raw_attributes": [...]}` must not leak
the marker key through the OCSF pipeline."""
def test_mitre_raw_attributes_pass_through_pipeline(self, tmp_path):
mitre_requirement = UniversalComplianceRequirement(
id="T1078",
description="Valid Accounts",
attributes={
"_raw_attributes": [{"AWSService": "IAM", "Category": "Initial Access"}]
},
checks={"aws": ["check_a"]},
)
framework = ComplianceFramework(
framework="MITRE",
name="MITRE ATT&CK",
version="14",
description="Mitre",
requirements=[mitre_requirement],
outputs=OutputsConfig(table_config=TableConfig(group_by="AWSService")),
)
generated = {"compliance": []}
process_universal_compliance_frameworks(
input_compliance_frameworks={"mitre_attack_aws"},
universal_frameworks={"mitre_attack_aws": framework},
finding_outputs=[_make_finding("check_a", "PASS")],
output_directory=str(tmp_path),
output_filename="out",
provider="aws",
generated_outputs=generated,
)
ocsf_path = tmp_path / "compliance" / "out_mitre_attack_aws.ocsf.json"
events = json.loads(ocsf_path.read_text())
assert isinstance(events, list) and len(events) >= 1
for event in events:
requirement_attrs = (event.get("unmapped") or {}).get(
"requirement_attributes", {}
)
assert "_raw_attributes" not in requirement_attrs
assert "raw_attributes" not in requirement_attrs
@@ -202,6 +202,26 @@ class TestOCSFComplianceOutput:
assert cf.status_code == "MANUAL"
assert cf.finding_info.uid == "manual-MANUAL-1"
def test_include_manual_false_skips_manual(self):
"""``_transform(..., include_manual=False)`` emits check events but
NOT manual requirement events. The streaming caller passes ``False``
for batches 2..N so manual events are not duplicated."""
covered = _simple_requirement("REQ-1", ["check_a"])
manual = _simple_requirement("MANUAL-1", checks=[])
fw = _make_framework([covered, manual])
findings = [_make_finding("check_a")]
output = OCSFComplianceOutput(findings=findings, framework=fw, provider="aws")
# __init__ transforms with include_manual=True (default) → manual present
assert any(cf.status_code == "MANUAL" for cf in output.data)
# A subsequent batch re-transforms with include_manual=False
output._data.clear()
output._transform(findings, fw, "TestFW-1.0", include_manual=False)
assert len(output.data) == 1 # only the check event, no manual
assert all(cf.status_code != "MANUAL" for cf in output.data)
def test_multi_provider_checks_dict(self):
req = UniversalComplianceRequirement(
id="REQ-1",
@@ -631,3 +651,103 @@ class TestNoTopLevelOCSFImport:
import prowler.lib.outputs.compliance.universal.ocsf_compliance as mod
assert "OCSF" not in dir(mod)
def _mitre_requirement(req_id="T1078", entries=None):
"""Build a MITRE-style requirement with `_raw_attributes` wrapping."""
return UniversalComplianceRequirement(
id=req_id,
description="Valid Accounts",
attributes={
"_raw_attributes": entries
or [{"AWSService": "IAM", "Category": "Initial Access"}]
},
checks={"aws": ["check_a"]},
)
class TestMitreRawAttributes:
"""MITRE attrs wrapped as `{"_raw_attributes": [...]}` must not leak
the marker key into the OCSF payload."""
def test_raw_attributes_key_not_in_unmapped(self):
framework = _make_framework([_mitre_requirement()])
findings = [_make_finding("check_a", "PASS")]
output = OCSFComplianceOutput(
findings=findings, framework=framework, provider="aws"
)
requirement_attrs = (output.data[0].unmapped or {}).get(
"requirement_attributes", {}
)
assert "_raw_attributes" not in requirement_attrs
assert "raw_attributes" not in requirement_attrs
def test_finding_serializes_with_raw_attributes(self):
framework = _make_framework(
[
_mitre_requirement(
entries=[
{"AWSService": "IAM", "Category": "Initial Access"},
{"AWSService": "STS", "Category": "Privilege Escalation"},
]
)
]
)
findings = [_make_finding("check_a", "PASS")]
output = OCSFComplianceOutput(
findings=findings, framework=framework, provider="aws"
)
compliance_finding = output.data[0]
if hasattr(compliance_finding, "model_dump_json"):
payload = json.loads(compliance_finding.model_dump_json(exclude_none=True))
else:
payload = json.loads(compliance_finding.json(exclude_none=True))
assert payload["compliance"]["requirements"] == ["T1078"]
class TestProviderFiltering:
"""OCSF writer scopes findings against `requirement.checks[provider]`."""
def test_check_for_other_provider_not_emitted(self):
azure_only_requirement = UniversalComplianceRequirement(
id="REQ-1",
description="Azure-only requirement",
attributes={},
checks={"azure": ["check_a"]},
)
framework = _make_framework([azure_only_requirement])
findings = [_make_finding("check_a", "PASS", provider="aws")]
output = OCSFComplianceOutput(
findings=findings, framework=framework, provider="aws"
)
assert all(
compliance_finding.status_code == "MANUAL"
for compliance_finding in output.data
)
def test_no_provider_aggregates_all_checks(self):
multi_provider_requirement = UniversalComplianceRequirement(
id="REQ-1",
description="Multi-provider requirement",
attributes={},
checks={"aws": ["check_a"], "azure": ["check_b"]},
)
framework = _make_framework([multi_provider_requirement])
findings = [
_make_finding("check_a", "PASS", provider="aws"),
_make_finding("check_b", "FAIL", provider="azure"),
]
output = OCSFComplianceOutput(
findings=findings, framework=framework, provider=None
)
statuses = sorted(
compliance_finding.status_code for compliance_finding in output.data
)
assert statuses == ["FAIL", "PASS"]
@@ -122,6 +122,43 @@ class TestManualRequirements:
assert manual_rows[0].dict()["Requirements_Id"] == "manual-1"
assert manual_rows[0].dict()["ResourceId"] == "manual_check"
def test_include_manual_false_skips_manual_rows(self, tmp_path):
"""``_transform(..., include_manual=False)`` emits finding rows but
NOT manual requirements. The streaming caller passes ``False`` for
batches 2..N so manual rows are not duplicated across batches."""
reqs = [
UniversalComplianceRequirement(
id="1.1",
description="test",
attributes={"Section": "IAM"},
checks={"aws": ["check_a"]},
),
UniversalComplianceRequirement(
id="manual-1",
description="manual check",
attributes={"Section": "Governance"},
checks={},
),
]
metadata = [AttributeMetadata(key="Section", type="str")]
fw = _make_framework(reqs, metadata, TableConfig(group_by="Section"))
findings = [_make_finding("check_a", "PASS", {"TestFW-1.0": ["1.1"]})]
output = UniversalComplianceOutput(
findings=findings,
framework=fw,
file_path=str(tmp_path / "t.csv"),
)
# __init__ transforms with include_manual=True (default) → manual present
assert any(r.dict()["Status"] == "MANUAL" for r in output.data)
# A subsequent batch re-transforms with include_manual=False
output._data.clear()
output._transform(findings, fw, "TestFW-1.0", include_manual=False)
assert len(output.data) == 1 # only the finding row, no manual
assert all(r.dict()["Status"] != "MANUAL" for r in output.data)
class TestMITREExtraColumns:
def test_mitre_columns_present(self, tmp_path):
+32
View File
@@ -2,8 +2,10 @@ import json
from datetime import datetime, timezone
from io import StringIO
from typing import Optional
from unittest.mock import MagicMock
from uuid import UUID
import pytest
import requests
from freezegun import freeze_time
from mock import patch
@@ -300,6 +302,36 @@ class TestOCSF:
def test_batch_write_data_to_file_without_findings(self):
assert not OCSF([])._file_descriptor
def test_batch_write_data_to_file_propagates_oserror(self):
"""An I/O error (e.g. ENOSPC) while writing a finding must propagate
instead of being swallowed, so the caller can fail fast."""
findings = [
generate_finding_output(
status="FAIL",
severity="low",
muted=False,
region=AWS_REGION_EU_WEST_1,
timestamp=datetime.now(),
resource_details="resource_details",
resource_name="resource_name",
resource_uid="resource-id",
status_extended="status extended",
)
]
output = OCSF(findings)
mock_file = MagicMock()
mock_file.closed = False
# Non-zero so the "[" prelude is skipped and the failure happens on the
# per-finding write, the exact path that hit ENOSPC in production.
mock_file.tell.return_value = 1
mock_file.write.side_effect = OSError(28, "No space left on device")
output._file_descriptor = mock_file
with pytest.raises(OSError) as excinfo:
output.batch_write_data_to_file()
assert excinfo.value.errno == 28
def test_finding_output_cloud_pass_low_muted(self):
finding_output = generate_finding_output(
status="PASS",
@@ -0,0 +1,229 @@
from unittest import mock
from prowler.providers.aws.services.sagemaker.sagemaker_service import (
MonitoringSchedule,
)
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
test_monitoring_schedule = "test-monitoring-schedule"
monitoring_schedule_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:monitoring-schedule/{test_monitoring_schedule}"
aggregate_name = "SageMaker Monitoring Schedules"
unknown_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:monitoring-schedule/unknown"
class Test_sagemaker_models_monitor_enabled:
def test_no_models_monitoring_schedules_exist(self):
sagemaker_client = mock.MagicMock
sagemaker_client.sagemaker_monitoring_schedules = [
MonitoringSchedule(
name=aggregate_name,
region=AWS_REGION_EU_WEST_1,
arn=unknown_arn,
has_schedules=False,
is_scheduled=False,
)
]
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled.sagemaker_client",
sagemaker_client,
),
):
from prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled import (
sagemaker_models_monitor_enabled,
)
check = sagemaker_models_monitor_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].region == AWS_REGION_EU_WEST_1
assert (
result[0].status_extended
== f"No SageMaker monitoring schedules found in region {AWS_REGION_EU_WEST_1}."
)
assert result[0].resource_id == aggregate_name
assert result[0].resource_arn == unknown_arn
def test_region_with_schedules_but_none_scheduled(self):
# A region that has monitoring schedules but none in Scheduled state
# must FAIL once.
sagemaker_client = mock.MagicMock
sagemaker_client.sagemaker_monitoring_schedules = [
MonitoringSchedule(
name=aggregate_name,
region=AWS_REGION_EU_WEST_1,
arn=unknown_arn,
has_schedules=True,
is_scheduled=False,
)
]
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled.sagemaker_client",
sagemaker_client,
),
):
from prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled import (
sagemaker_models_monitor_enabled,
)
check = sagemaker_models_monitor_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].region == AWS_REGION_EU_WEST_1
assert (
result[0].status_extended
== f"No active SageMaker monitoring schedule in region {AWS_REGION_EU_WEST_1}; existing schedules are not in Scheduled status."
)
def test_region_with_one_scheduled_passes(self):
sagemaker_client = mock.MagicMock
sagemaker_client.sagemaker_monitoring_schedules = [
MonitoringSchedule(
name=test_monitoring_schedule,
region=AWS_REGION_EU_WEST_1,
arn=monitoring_schedule_arn,
has_schedules=True,
is_scheduled=True,
)
]
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled.sagemaker_client",
sagemaker_client,
),
):
from prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled import (
sagemaker_models_monitor_enabled,
)
check = sagemaker_models_monitor_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].region == AWS_REGION_EU_WEST_1
assert (
result[0].status_extended
== f"SageMaker monitoring schedule {test_monitoring_schedule} is enabled in region {AWS_REGION_EU_WEST_1}."
)
assert result[0].resource_id == test_monitoring_schedule
assert result[0].resource_arn == monitoring_schedule_arn
def test_scheduled_not_masked_across_regions(self):
# Regression: a region without an active monitor must not mask a
# Scheduled monitor in another region; one finding per region.
scheduled_name = "scheduled-monitor"
scheduled_arn = f"arn:aws:sagemaker:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:monitoring-schedule/{scheduled_name}"
sagemaker_client = mock.MagicMock
sagemaker_client.sagemaker_monitoring_schedules = [
MonitoringSchedule(
name=aggregate_name,
region=AWS_REGION_EU_WEST_1,
arn=unknown_arn,
has_schedules=False,
is_scheduled=False,
),
MonitoringSchedule(
name=scheduled_name,
region=AWS_REGION_US_EAST_1,
arn=scheduled_arn,
has_schedules=True,
is_scheduled=True,
),
]
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled.sagemaker_client",
sagemaker_client,
),
):
from prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled import (
sagemaker_models_monitor_enabled,
)
check = sagemaker_models_monitor_enabled()
result = check.execute()
assert len(result) == 2
results_by_region = {r.region: r for r in result}
assert results_by_region[AWS_REGION_EU_WEST_1].status == "FAIL"
assert (
results_by_region[AWS_REGION_EU_WEST_1].status_extended
== f"No SageMaker monitoring schedules found in region {AWS_REGION_EU_WEST_1}."
)
assert results_by_region[AWS_REGION_US_EAST_1].status == "PASS"
assert (
results_by_region[AWS_REGION_US_EAST_1].status_extended
== f"SageMaker monitoring schedule {scheduled_name} is enabled in region {AWS_REGION_US_EAST_1}."
)
def test_empty_schedules_list(self):
# Regression: an empty list must not raise and must yield no findings.
sagemaker_client = mock.MagicMock
sagemaker_client.sagemaker_monitoring_schedules = []
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled.sagemaker_client",
sagemaker_client,
),
):
from prowler.providers.aws.services.sagemaker.sagemaker_models_monitor_enabled.sagemaker_models_monitor_enabled import (
sagemaker_models_monitor_enabled,
)
check = sagemaker_models_monitor_enabled()
result = check.execute()
assert result == []