feat: add categories and resource group validator

This commit is contained in:
Daniel Barranquero
2026-03-13 14:24:10 +01:00
parent 0cef9eb375
commit 6e4027bb21
10 changed files with 393 additions and 116 deletions
+110 -35
View File
@@ -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):
+11 -2
View File
@@ -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": {},
+11 -6
View File
@@ -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,
+2 -2
View File
@@ -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",
@@ -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="",
+245 -57
View File
@@ -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):
+4 -4
View File
@@ -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)
+7 -7
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": [