mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
149 lines
5.8 KiB
Python
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
|