mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-30 23:34:27 +00:00
Merge remote-tracking branch 'origin/PROWLER-1778-dynamic-output-report-generation' into PROWLER-1799-sdk-only-provider-property
This commit is contained in:
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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
+229
@@ -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 == []
|
||||
Reference in new issue
Block a user