Files
prowler/tests/lib/check/mitre_config_requirements_test.py

149 lines
5.8 KiB
Python

"""Regression coverage for ConfigRequirements on MITRE requirements.
``mitre_attack_aws.json`` declares ``ConfigRequirements`` on its requirements,
but ``Mitre_Requirement`` historically did not define the field, so Pydantic
silently dropped the constraints during MITRE parsing and the config validation
logic never saw them. These tests prove the constraints survive parsing and that
a violated MITRE config requirement forces the compliance result to FAIL through
the universal output path.
"""
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import patch
from py_ocsf_models.objects.compliance_status import StatusID as ComplianceStatusID
from prowler.lib.check.compliance_models import (
Compliance,
Mitre_Requirement,
adapt_legacy_to_universal,
)
from prowler.lib.outputs.compliance.universal.ocsf_compliance import (
OCSFComplianceOutput,
)
_MODULE = "prowler.providers.common.provider.Provider.get_global_provider"
def _mitre_compliance(check_id):
"""A minimal one-requirement MITRE framework with a config constraint."""
return Compliance(
Framework="MITRE-ATTACK",
Name="MITRE ATT&CK",
Provider="AWS",
Version="",
Description="Test MITRE framework",
Requirements=[
{
"Name": "Test Technique",
"Id": "T9999",
"Tactics": ["initial-access"],
"SubTechniques": [],
"Platforms": ["AWS"],
"Description": "Requirement T9999",
"TechniqueURL": "https://attack.mitre.org/techniques/T9999",
"Checks": [check_id],
"ConfigRequirements": [
{
"Check": check_id,
"ConfigKey": "mute_non_default_regions",
"Operator": "eq",
"Value": False,
}
],
"Attributes": [
{
"AWSService": "service",
"Category": "category",
"Value": "value",
"Comment": "comment",
}
],
}
],
)
def _finding(check_id, status="PASS", provider="aws"):
finding = SimpleNamespace()
finding.provider = provider
finding.account_uid = "123456789012"
finding.account_name = "test-account"
finding.account_organization_uid = "org-123"
finding.account_organization_name = "test-org"
finding.region = "us-east-1"
finding.status = status
finding.status_extended = f"{check_id} is {status}"
finding.resource_uid = f"arn:aws:iam::123456789012:{check_id}"
finding.resource_name = check_id
finding.resource_details = "details"
finding.resource_metadata = {}
finding.resource_tags = {"Name": "test"}
finding.partition = "aws"
finding.muted = False
finding.check_id = check_id
finding.uid = "test-finding-uid"
finding.timestamp = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
finding.prowler_version = "5.0.0"
finding.metadata = SimpleNamespace(
CheckID=check_id,
CheckTitle=f"Title for {check_id}",
Description=f"Description for {check_id}",
Severity="medium",
ServiceName="iam",
ResourceType="aws-iam-role",
)
return finding
class Test_Mitre_Config_Requirements:
def test_config_requirements_survive_mitre_parsing(self):
"""Real mitre_attack_aws.json constraints must not be dropped on parse."""
compliance = Compliance.parse_file(
"prowler/compliance/aws/mitre_attack_aws.json"
)
requirement = next(r for r in compliance.Requirements if r.Id == "T1190")
assert isinstance(requirement, Mitre_Requirement)
assert requirement.ConfigRequirements
# And they propagate through the legacy -> universal adapter unchanged.
universal = adapt_legacy_to_universal(compliance)
universal_requirement = next(
r for r in universal.requirements if r.id == "T1190"
)
assert universal_requirement.config_requirements
assert len(universal_requirement.config_requirements) == len(
requirement.ConfigRequirements
)
def test_violating_mitre_config_forces_fail(self):
"""A PASS finding becomes FAIL when the MITRE config constraint is violated."""
check_id = "drs_job_exist"
framework = adapt_legacy_to_universal(_mitre_compliance(check_id))
findings = [_finding(check_id, "PASS")]
with patch(_MODULE) as mock_gp:
mock_gp.return_value.audit_config = {"mute_non_default_regions": True}
out = OCSFComplianceOutput(
findings=findings, framework=framework, provider="aws"
)
event = out.data[0]
assert event.compliance.status_id == ComplianceStatusID.Fail
assert event.status_code == "FAIL"
assert "Configuration not valid" in event.message
# The nested Check object keeps the real (raw) finding status.
assert event.compliance.checks[0].status == "PASS"
def test_valid_mitre_config_keeps_pass(self):
check_id = "drs_job_exist"
framework = adapt_legacy_to_universal(_mitre_compliance(check_id))
findings = [_finding(check_id, "PASS")]
with patch(_MODULE) as mock_gp:
mock_gp.return_value.audit_config = {"mute_non_default_regions": False}
out = OCSFComplianceOutput(
findings=findings, framework=framework, provider="aws"
)
event = out.data[0]
assert event.compliance.status_id == ComplianceStatusID.Pass
assert event.status_code == "PASS"
assert "Configuration not valid" not in event.message