From 6e4027bb214b3622b06b5bfb55efbf8941ff4c24 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero Date: Fri, 13 Mar 2026 14:24:10 +0100 Subject: [PATCH] feat: add categories and resource group validator --- prowler/lib/check/models.py | 145 +++++++-- prowler/providers/aws/config/check_types.json | 13 +- tests/lib/check/check_loader_test.py | 17 +- tests/lib/check/compliance_check_test.py | 4 +- .../check/fixtures/bulk_checks_metadata.py | 2 +- tests/lib/check/models_test.py | 302 ++++++++++++++---- tests/lib/outputs/csv/csv_test.py | 8 +- tests/lib/outputs/finding_test.py | 14 +- tests/lib/outputs/fixtures/fixtures.py | 2 +- tests/lib/outputs/ocsf/ocsf_test.py | 2 +- 10 files changed, 393 insertions(+), 116 deletions(-) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 7052d9c4dd..6cb1c210a9 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -1,4 +1,5 @@ import functools +import json import os import re import sys @@ -15,6 +16,87 @@ from prowler.lib.check.compliance_models import Compliance from prowler.lib.check.utils import recover_checks_from_provider from prowler.lib.logger import logger +# Valid ResourceGroup values as defined in the RFC +VALID_RESOURCE_GROUPS = frozenset( + { + "compute", + "container", + "serverless", + "database", + "storage", + "network", + "IAM", + "messaging", + "security", + "monitoring", + "api_gateway", + "ai_ml", + "governance", + "collaboration", + "devops", + "analytics", + } +) + +# Valid Categories as defined in the RFC +VALID_CATEGORIES = frozenset( + { + "encryption", + "internet-exposed", + "logging", + "secrets", + "resilience", + "threat-detection", + "trust-boundaries", + "vulnerabilities", + "cluster-security", + "container-security", + "node-security", + "gen-ai", + "ci-cd", + "identity-access", + "email-security", + "forensics-ready", + "software-supply-chain", + "e3", + "e5", + "privilege-escalation", + "ec2-imdsv1", + } +) + + +@functools.lru_cache(maxsize=1) +def _load_aws_check_types_hierarchy() -> dict: + """ + Load and cache the AWS CheckTypes hierarchy from the JSON config file. + + Returns: + dict: The CheckTypes hierarchy, or empty dict if file not found. + """ + try: + current_dir = os.path.dirname(os.path.abspath(__file__)) + check_types_file = os.path.normpath( + os.path.join( + current_dir, + "..", + "..", + "providers", + "aws", + "config", + "check_types.json", + ) + ) + + if not os.path.exists(check_types_file): + return {} + + with open(check_types_file, "r") as f: + return json.load(f) + + except (FileNotFoundError, json.JSONDecodeError): + return {} + def _validate_aws_check_type_in_config(check_type: str) -> bool: """ @@ -27,42 +109,22 @@ def _validate_aws_check_type_in_config(check_type: str) -> bool: Returns: bool: True if the CheckType path exists in the config hierarchy """ - try: - import json - import os - - if not check_type: - return False - - # Get the path to the AWS CheckTypes configuration - current_dir = os.path.dirname(os.path.abspath(__file__)) - check_types_file = os.path.join( - current_dir, "..", "..", "providers", "aws", "config", "check_types.json" - ) - check_types_file = os.path.normpath(check_types_file) - - # Load the CheckTypes hierarchy from JSON file - if not os.path.exists(check_types_file): - return False - - with open(check_types_file, "r") as f: - hierarchy = json.load(f) - - # Split the path by '/' to get each level - path_parts = check_type.split("/") - - # Navigate through the hierarchy using direct lookups - current_level = hierarchy - for part in path_parts: - if not isinstance(current_level, dict) or part not in current_level: - return False - current_level = current_level[part] - - return True - - except (KeyError, AttributeError, FileNotFoundError, json.JSONDecodeError): + if not check_type: return False + hierarchy = _load_aws_check_types_hierarchy() + if not hierarchy: + return False + + path_parts = check_type.split("/") + current_level = hierarchy + for part in path_parts: + if not isinstance(current_level, dict) or part not in current_level: + return False + current_level = current_level[part] + + return True + class Code(BaseModel): """ @@ -142,7 +204,7 @@ class CheckMetadata(BaseModel): Compliance (list, optional): The compliance information for the check. Defaults to None. Validators: - valid_category(value): Validator function to validate the categories of the check. + valid_category(value): Validator function to validate the categories of the check against predefined values. severity_to_lower(severity): Validator function to convert the severity to lowercase. valid_cli_command(remediation): Validator function to validate the CLI command is not an URL. valid_resource_type(resource_type): Validator function to validate the resource type is not empty. @@ -152,6 +214,7 @@ class CheckMetadata(BaseModel): validate_check_type(check_type, values): Validator function to validate CheckType - no empty strings for all providers, plus predefined types validation for AWS (loaded from config file). validate_description(description): Validator function to validate Description max length (400 chars). validate_risk(risk): Validator function to validate Risk max length (400 chars). + validate_resource_group(resource_group): Validator function to validate ResourceGroup against predefined values. validate_additional_urls(additional_urls): Validator function to ensure AdditionalURLs contains no duplicates. """ @@ -188,6 +251,10 @@ class CheckMetadata(BaseModel): raise ValueError( f"Invalid category: {value}. Categories can only contain lowercase letters, numbers and hyphen '-'" ) + if value_lower not in VALID_CATEGORIES: + raise ValueError( + f"Invalid category: '{value_lower}'. Must be one of: {', '.join(sorted(VALID_CATEGORIES))}." + ) return value_lower @validator("Severity", pre=True, always=True) @@ -281,6 +348,14 @@ class CheckMetadata(BaseModel): ) return risk + @validator("ResourceGroup", pre=True, always=True) + def validate_resource_group(cls, resource_group): + if resource_group and resource_group not in VALID_RESOURCE_GROUPS: + raise ValueError( + f"Invalid ResourceGroup: '{resource_group}'. Must be one of: {', '.join(sorted(VALID_RESOURCE_GROUPS))} or empty string." + ) + return resource_group + @validator("AdditionalURLs", pre=True, always=True) def validate_additional_urls(cls, additional_urls): if not isinstance(additional_urls, list): diff --git a/prowler/providers/aws/config/check_types.json b/prowler/providers/aws/config/check_types.json index 23f1f47e16..ce43c32e23 100644 --- a/prowler/providers/aws/config/check_types.json +++ b/prowler/providers/aws/config/check_types.json @@ -5,7 +5,11 @@ }, "AWS Security Best Practices": { "Network Reachability": {}, - "Runtime Behavior Analysis": {} + "Network Security": {}, + "Runtime Behavior Analysis": {}, + "Data Encryption": {}, + "Encryption at Rest": {}, + "Encryption in Transit": {} }, "Industry and Regulatory Standards": { "AWS Foundational Security Best Practices": {}, @@ -20,6 +24,7 @@ "SOC 1": {}, "SOC 2": {}, "HIPAA Controls (USA)": {}, + "NIST 800-53 Controls": {}, "NIST 800-53 Controls (USA)": {}, "NIST CSF Controls (USA)": {}, "IRAP Controls (Australia)": {}, @@ -38,7 +43,11 @@ "Patch Management": {} }, "TTPs": { - "Initial Access": {}, + "Initial Access": { + "Unauthorized Access": {}, + "External Remote Services": {}, + "Valid Accounts": {} + }, "Execution": {}, "Persistence": {}, "Privilege Escalation": {}, diff --git a/tests/lib/check/check_loader_test.py b/tests/lib/check/check_loader_test.py index 1b61be5115..f082557778 100644 --- a/tests/lib/check/check_loader_test.py +++ b/tests/lib/check/check_loader_test.py @@ -268,12 +268,17 @@ class TestCheckLoader: } compliance_frameworks = ["soc2_aws"] - assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metadata, - bulk_compliance_frameworks=bulk_compliance_frameworks, - compliance_frameworks=compliance_frameworks, - provider=self.provider, - ) + # Mock get_bulk to prevent loading real metadata files that may fail validation + with patch( + "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", + return_value=bulk_checks_metadata, + ): + assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( + bulk_checks_metadata=bulk_checks_metadata, + bulk_compliance_frameworks=bulk_compliance_frameworks, + compliance_frameworks=compliance_frameworks, + provider=self.provider, + ) def test_load_checks_to_execute_with_categories( self, diff --git a/tests/lib/check/compliance_check_test.py b/tests/lib/check/compliance_check_test.py index f36707da21..9e45fb8acc 100644 --- a/tests/lib/check/compliance_check_test.py +++ b/tests/lib/check/compliance_check_test.py @@ -215,7 +215,7 @@ class TestCompliance: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -243,7 +243,7 @@ class TestCompliance: }, "Recommendation": {"Text": "text2", "Url": "url2"}, }, - Categories=["categorytwo"], + Categories=["logging"], DependsOn=["dependency2"], RelatedTo=["related2"], Notes="notes2", diff --git a/tests/lib/check/fixtures/bulk_checks_metadata.py b/tests/lib/check/fixtures/bulk_checks_metadata.py index 2de2ca2a2a..878d694a75 100644 --- a/tests/lib/check/fixtures/bulk_checks_metadata.py +++ b/tests/lib/check/fixtures/bulk_checks_metadata.py @@ -87,7 +87,7 @@ test_bulk_checks_metadata = { Text="Ensure all vpc has public and private subnets defined", Url="" ), ), - Categories=["internet-exposed", "trustboundaries"], + Categories=["internet-exposed", "trust-boundaries"], DependsOn=[], RelatedTo=[], Notes="", diff --git a/tests/lib/check/models_test.py b/tests/lib/check/models_test.py index f24fe91727..4726cd3f04 100644 --- a/tests/lib/check/models_test.py +++ b/tests/lib/check/models_test.py @@ -8,10 +8,10 @@ from prowler.lib.check.models import Check, CheckMetadata from tests.lib.check.compliance_check_test import custom_compliance_metadata mock_metadata = CheckMetadata( - Provider="azure", # Using non-AWS provider to avoid config validation issues + Provider="aws", CheckID="accessanalyzer_enabled", CheckTitle="Check 1", - CheckType=["Security"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="accessanalyzer", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -29,7 +29,7 @@ mock_metadata = CheckMetadata( }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -37,10 +37,10 @@ mock_metadata = CheckMetadata( ) mock_metadata_lambda = CheckMetadata( - Provider="azure", # Using non-AWS provider to avoid config validation issues + Provider="aws", CheckID="awslambda_function_url_public", CheckTitle="Check 1", - CheckType=["Security"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="awslambda", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -58,7 +58,7 @@ mock_metadata_lambda = CheckMetadata( }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -175,7 +175,7 @@ class TestCheckMetada: bulk_metadata = CheckMetadata.get_bulk(provider="aws") result = CheckMetadata.list( - bulk_checks_metadata=bulk_metadata, category="categoryone" + bulk_checks_metadata=bulk_metadata, category="encryption" ) # Assertions @@ -363,7 +363,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security", "network", "data-protection"], + "Categories": ["encryption", "logging", "secrets"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -371,7 +371,7 @@ class TestCheckMetadataValidators: # Should not raise any validation error check_metadata = CheckMetadata(**valid_metadata) - assert check_metadata.Categories == ["security", "network", "data-protection"] + assert check_metadata.Categories == ["encryption", "logging", "secrets"] def test_valid_category_failure_non_string(self): """Test valid category validation fails with non-string category""" @@ -454,6 +454,85 @@ class TestCheckMetadataValidators: in str(exc_info.value) ) + def test_valid_category_failure_not_predefined(self): + """Test valid category validation fails with non-predefined category""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "https://example.com", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://example.com", + }, + }, + "Categories": ["not-a-real-category"], # Invalid: not in predefined list + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Invalid category: 'not-a-real-category'. Must be one of:" in str( + exc_info.value + ) + + def test_valid_category_all_predefined_values(self): + """Test that all predefined categories are accepted""" + from prowler.lib.check.models import VALID_CATEGORIES + + for category in VALID_CATEGORIES: + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": ["Security"], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "https://example.com", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://example.com", + }, + }, + "Categories": [category], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + check_metadata = CheckMetadata(**valid_metadata) + assert category in check_metadata.Categories + def test_severity_to_lower_success(self): """Test severity validation converts to lowercase""" valid_metadata = { @@ -483,7 +562,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -521,7 +600,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -560,7 +639,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -599,7 +678,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -637,7 +716,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -676,7 +755,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -714,7 +793,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -754,7 +833,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -793,7 +872,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -831,7 +910,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -869,7 +948,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -908,7 +987,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -950,7 +1029,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -988,7 +1067,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1027,7 +1106,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1063,7 +1142,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1102,7 +1181,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1140,7 +1219,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1181,7 +1260,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1219,7 +1298,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1259,7 +1338,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1297,7 +1376,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1333,7 +1412,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1370,7 +1449,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1406,7 +1485,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1443,7 +1522,7 @@ class TestCheckMetadataValidators: "Url": "https://example.com", }, }, - "Categories": ["security"], + "Categories": ["encryption"], "DependsOn": [], "RelatedTo": [], "Notes": "Test notes", @@ -1459,7 +1538,7 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1477,7 +1556,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1497,7 +1576,7 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1515,7 +1594,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1531,7 +1610,9 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1549,7 +1630,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1565,7 +1646,9 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1583,7 +1666,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1599,7 +1682,9 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1617,7 +1702,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1633,7 +1718,9 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1651,7 +1738,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1670,7 +1757,7 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check_empty_fields", CheckTitle="Test Check with Empty Fields", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1688,7 +1775,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1706,7 +1793,7 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check_defaults", CheckTitle="Test Check with Default Fields", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1724,7 +1811,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1743,7 +1830,9 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check_none_related_url", CheckTitle="Test Check with None RelatedUrl", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1761,7 +1850,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1778,7 +1867,9 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check_none_additional_urls", CheckTitle="Test Check with None AdditionalURLs", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1796,7 +1887,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1813,7 +1904,9 @@ class TestCheckMetadataValidators: Provider="aws", CheckID="test_check_invalid_additional_urls", CheckTitle="Test Check with Invalid AdditionalURLs", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -1831,7 +1924,7 @@ class TestCheckMetadataValidators: }, "Recommendation": {"Text": "text1", "Url": "url1"}, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -1842,6 +1935,101 @@ class TestCheckMetadataValidators: assert "AdditionalURLs must be a list" in str(exc_info.value) +class TestResourceGroupValidator: + """Test class for ResourceGroup validator""" + + def _base_metadata(self, **overrides): + """Helper to build valid metadata with overrides""" + base = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "https://example.com", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://example.com", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + base.update(overrides) + return base + + @pytest.mark.parametrize( + "resource_group", + [ + "compute", + "container", + "serverless", + "database", + "storage", + "network", + "IAM", + "messaging", + "security", + "monitoring", + "api_gateway", + "ai_ml", + "governance", + "collaboration", + "devops", + "analytics", + ], + ) + def test_valid_resource_group(self, resource_group): + """Test all valid ResourceGroup values are accepted""" + metadata = CheckMetadata(**self._base_metadata(ResourceGroup=resource_group)) + assert metadata.ResourceGroup == resource_group + + def test_resource_group_empty_string_allowed(self): + """Test that empty string (default) is allowed for ResourceGroup""" + metadata = CheckMetadata(**self._base_metadata(ResourceGroup="")) + assert metadata.ResourceGroup == "" + + def test_resource_group_default_is_empty(self): + """Test that ResourceGroup defaults to empty string when not provided""" + metadata = CheckMetadata(**self._base_metadata()) + assert metadata.ResourceGroup == "" + + def test_resource_group_invalid_value(self): + """Test that invalid ResourceGroup value raises ValidationError""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**self._base_metadata(ResourceGroup="invalid_group")) + assert "Invalid ResourceGroup: 'invalid_group'" in str(exc_info.value) + + def test_resource_group_case_sensitive(self): + """Test that ResourceGroup validation is case-sensitive (IAM, not iam)""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**self._base_metadata(ResourceGroup="iam")) + assert "Invalid ResourceGroup: 'iam'" in str(exc_info.value) + + def test_resource_group_typo(self): + """Test that typos in ResourceGroup are rejected""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**self._base_metadata(ResourceGroup="computee")) + assert "Invalid ResourceGroup: 'computee'" in str(exc_info.value) + + class TestCheck: @mock.patch("prowler.lib.check.models.CheckMetadata.parse_file") def test_verify_names_consistency_all_match(self, mock_parse_file): diff --git a/tests/lib/outputs/csv/csv_test.py b/tests/lib/outputs/csv/csv_test.py index eac514c42d..3b06ce787a 100644 --- a/tests/lib/outputs/csv/csv_test.py +++ b/tests/lib/outputs/csv/csv_test.py @@ -37,7 +37,7 @@ class TestCSV: remediation_code_other="other-code", remediation_code_cli="cli-code", compliance={"compliance_key": "compliance_value"}, - categories=["categorya", "categoryb"], + categories=["encryption", "logging"], depends_on=["dependency"], related_to=["related"], additional_urls=[ @@ -101,7 +101,7 @@ class TestCSV: assert output_data["REMEDIATION_CODE_OTHER"] == "other-code" assert isinstance(output_data["COMPLIANCE"], str) assert output_data["COMPLIANCE"] == "compliance_key: compliance_value" - assert output_data["CATEGORIES"] == "categorya | categoryb" + assert output_data["CATEGORIES"] == "encryption | logging" assert output_data["DEPENDS_ON"] == "dependency" assert output_data["RELATED_TO"] == "related" assert ( @@ -126,7 +126,7 @@ class TestCSV: output.batch_write_data_to_file() mock_file.seek(0) - expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS;ACCOUNT_OU_UID;ACCOUNT_OU_NAME\r\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;test-type;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;test-url;;;;;;;test-compliance: test-compliance;test-category;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html;ou-abc1-12345678;Production/WebServices\r\n" + expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS;ACCOUNT_OU_UID;ACCOUNT_OU_NAME\r\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;Software and Configuration Checks/AWS Security Best Practices/Network Reachability;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;test-url;;;;;;;test-compliance: test-compliance;encryption;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html;ou-abc1-12345678;Production/WebServices\r\n" content = mock_file.read() assert content == expected_csv @@ -204,7 +204,7 @@ class TestCSV: with patch.object(temp_file, "close", return_value=None): csv.batch_write_data_to_file() - expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS;ACCOUNT_OU_UID;ACCOUNT_OU_NAME\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;test-type;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;test-url;;;;;;;test-compliance: test-compliance;test-category;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html;ou-abc1-12345678;Production/WebServices\n" + expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS;ACCOUNT_OU_UID;ACCOUNT_OU_NAME\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;Software and Configuration Checks/AWS Security Best Practices/Network Reachability;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;test-url;;;;;;;test-compliance: test-compliance;encryption;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html;ou-abc1-12345678;Production/WebServices\n" temp_file.seek(0) diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index 461b3f3bd9..8a2aadb97b 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -910,7 +910,7 @@ class TestFinding: "provider": "test_provider", "checkid": "service_check_001", "checktitle": "Test Check", - "checktype": [], + "checktype": ["Security"], "servicename": "service", "subservicename": "SubService", "severity": "high", @@ -928,7 +928,7 @@ class TestFinding: }, }, "resourceidtemplate": "template", - "categories": ["cat-one", "cat-two"], + "categories": ["encryption", "logging"], "dependson": ["dep1"], "relatedto": ["rel1"], "notes": "Some notes", @@ -953,7 +953,7 @@ class TestFinding: assert meta.Provider == "test_provider" assert meta.CheckID == "service_check_001" assert meta.CheckTitle == "Test Check" - assert meta.CheckType == [] + assert meta.CheckType == ["Security"] assert meta.ServiceName == "service" assert meta.SubServiceName == "SubService" assert meta.Severity == "high" @@ -968,7 +968,7 @@ class TestFinding: assert meta.Remediation.Code.CLI == "cli_code" assert meta.Remediation.Code.Other == "other_code" assert meta.ResourceIdTemplate == "template" - assert meta.Categories == ["cat-one", "cat-two"] + assert meta.Categories == ["encryption", "logging"] assert meta.DependsOn == ["dep1"] assert meta.RelatedTo == ["rel1"] assert meta.Notes == "Some notes" @@ -1167,7 +1167,7 @@ class TestFinding: "recommendation": {"text": "Fix it", "url": "http://fix-gcp.com"}, }, "resourceidtemplate": "template", - "categories": ["cat-one", "cat-two"], + "categories": ["encryption", "logging"], "dependson": ["dep1"], "relatedto": ["rel1"], "notes": "Some notes", @@ -1247,7 +1247,7 @@ class TestFinding: "recommendation": {"text": "Fix it", "url": "http://fix-k8s.com"}, }, "resourceidtemplate": "template", - "categories": ["cat-one"], + "categories": ["encryption"], "dependson": [], "relatedto": [], "notes": "K8s notes", @@ -1313,7 +1313,7 @@ class TestFinding: "recommendation": {"text": "Fix it", "url": "http://fix-m365.com"}, }, "resourceidtemplate": "template", - "categories": ["cat-one"], + "categories": ["encryption"], "dependson": [], "relatedto": [], "notes": "M365 notes", diff --git a/tests/lib/outputs/fixtures/fixtures.py b/tests/lib/outputs/fixtures/fixtures.py index 527f3c94aa..6d7881e6fd 100644 --- a/tests/lib/outputs/fixtures/fixtures.py +++ b/tests/lib/outputs/fixtures/fixtures.py @@ -32,7 +32,7 @@ def generate_finding_output( remediation_code_terraform: str = "", remediation_code_cli: str = "", remediation_code_other: str = "", - categories: list[str] = ["test-category"], + categories: list[str] = ["encryption"], depends_on: list[str] = ["test-dependency"], related_to: list[str] = ["test-related-to"], notes: str = "test-notes", diff --git a/tests/lib/outputs/ocsf/ocsf_test.py b/tests/lib/outputs/ocsf/ocsf_test.py index 643260ff18..b101dec9a7 100644 --- a/tests/lib/outputs/ocsf/ocsf_test.py +++ b/tests/lib/outputs/ocsf/ocsf_test.py @@ -214,7 +214,7 @@ class TestOCSF: "status_id": 1, "unmapped": { "related_url": "test-url", - "categories": ["test-category"], + "categories": ["encryption"], "depends_on": ["test-dependency"], "related_to": ["test-related-to"], "additional_urls": [