diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index fa05248663..484f4cf41c 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -34,6 +34,13 @@ All notable changes to the **Prowler SDK** are documented in this file. --- +## [v5.12.0] (Prowler UNRELEASED) + +### Added +- `AdditionalUrls` field in CheckMetadata [(#8590)](https://github.com/prowler-cloud/prowler/pull/8590) + +--- + ## [v5.11.0] (Prowler v5.11.0) ### Added diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index de38162cbc..547dc1d94b 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -7,7 +7,7 @@ from dataclasses import asdict, dataclass, is_dataclass from enum import Enum from typing import Any, Dict, Optional, Set -from pydantic.v1 import BaseModel, ValidationError, validator +from pydantic.v1 import BaseModel, Field, ValidationError, validator from prowler.config.config import Provider from prowler.lib.check.compliance_models import Compliance @@ -85,6 +85,7 @@ class CheckMetadata(BaseModel): Risk (str): The risk associated with the check. RelatedUrl (str): The URL related to the check. Remediation (Remediation): The remediation steps for the check. + AdditionalUrls (list[str]): Additional URLs related to the check. Defaults to an empty list. Categories (list[str]): The categories of the check. DependsOn (list[str]): The dependencies of the check. RelatedTo (list[str]): The related checks. @@ -97,13 +98,14 @@ class CheckMetadata(BaseModel): valid_severity(severity): Validator function to validate the severity of the check. 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. + validate_additional_urls(additional_urls): Validator function to ensure AdditionalUrls contains no duplicates. """ Provider: str CheckID: str CheckTitle: str CheckType: list[str] - CheckAliases: list[str] = [] + CheckAliases: list[str] = Field(default_factory=list) ServiceName: str SubServiceName: str ResourceIdTemplate: str @@ -113,13 +115,14 @@ class CheckMetadata(BaseModel): Risk: str RelatedUrl: str Remediation: Remediation + AdditionalUrls: list[str] = Field(default_factory=list) Categories: list[str] DependsOn: list[str] RelatedTo: list[str] Notes: str # We set the compliance to None to # store the compliance later if supplied - Compliance: Optional[list[Any]] = [] + Compliance: Optional[list[Any]] = Field(default_factory=list) @validator("Categories", each_item=True, pre=True, always=True) def valid_category(value): @@ -178,6 +181,19 @@ class CheckMetadata(BaseModel): return check_id + @validator("AdditionalUrls", pre=True, always=True) + def validate_additional_urls(cls, additional_urls): + if not isinstance(additional_urls, list): + raise ValueError("AdditionalUrls must be a list") + + if any(not url or not url.strip() for url in additional_urls): + raise ValueError("AdditionalUrls cannot contain empty items") + + if len(additional_urls) != len(set(additional_urls)): + raise ValueError("AdditionalUrls cannot contain duplicate items") + + return additional_urls + @staticmethod def get_bulk(provider: str) -> dict[str, "CheckMetadata"]: """ diff --git a/tests/lib/check/models_test.py b/tests/lib/check/models_test.py index 83b294f171..341d696ee3 100644 --- a/tests/lib/check/models_test.py +++ b/tests/lib/check/models_test.py @@ -1,5 +1,8 @@ from unittest import mock +import pytest +from pydantic.v1 import ValidationError + from prowler.lib.check.models import CheckMetadata from tests.lib.check.compliance_check_test import custom_compliance_metadata @@ -325,3 +328,391 @@ class TestCheckMetada: result = CheckMetadata.list(bulk_checks_metadata=bulk_metadata) assert result == set() + + def test_additional_urls_valid_empty_list(self): + """Test AdditionalUrls with valid empty list (default)""" + metadata = CheckMetadata( + Provider="aws", + CheckID="test_check", + CheckTitle="Test Check", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=[], + Compliance=[], + ) + assert metadata.AdditionalUrls == [] + + def test_additional_urls_valid_with_urls(self): + """Test AdditionalUrls with valid URLs""" + valid_urls = [ + "https://example.com/doc1", + "https://example.com/doc2", + "https://aws.amazon.com/docs", + ] + metadata = CheckMetadata( + Provider="aws", + CheckID="test_check", + CheckTitle="Test Check", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=valid_urls, + Compliance=[], + ) + assert metadata.AdditionalUrls == valid_urls + + def test_additional_urls_invalid_not_list(self): + """Test AdditionalUrls with non-list value""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata( + Provider="aws", + CheckID="test_check", + CheckTitle="Test Check", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls="not_a_list", + Compliance=[], + ) + assert "AdditionalUrls must be a list" in str(exc_info.value) + + def test_additional_urls_invalid_empty_items(self): + """Test AdditionalUrls with empty string items""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata( + Provider="aws", + CheckID="test_check", + CheckTitle="Test Check", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=["https://example.com", "", "https://example2.com"], + Compliance=[], + ) + assert "AdditionalUrls cannot contain empty items" in str(exc_info.value) + + def test_additional_urls_invalid_whitespace_items(self): + """Test AdditionalUrls with whitespace-only items""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata( + Provider="aws", + CheckID="test_check", + CheckTitle="Test Check", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=["https://example.com", " ", "https://example2.com"], + Compliance=[], + ) + assert "AdditionalUrls cannot contain empty items" in str(exc_info.value) + + def test_additional_urls_invalid_duplicates(self): + """Test AdditionalUrls with duplicate items""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata( + Provider="aws", + CheckID="test_check", + CheckTitle="Test Check", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=[ + "https://example.com", + "https://example2.com", + "https://example.com", + ], + Compliance=[], + ) + assert "AdditionalUrls cannot contain duplicate items" in str(exc_info.value) + + def test_fields_with_explicit_empty_values(self): + """Test that RelatedUrl and AdditionalUrls can be set to explicit empty values""" + metadata = CheckMetadata( + Provider="aws", + CheckID="test_check_empty_fields", + CheckTitle="Test Check with Empty Fields", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="", # Explicit empty string + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=[], # Explicit empty list + Compliance=[], + ) + + # Assert that the fields are set to empty values + assert metadata.RelatedUrl == "" + assert metadata.AdditionalUrls == [] + + def test_fields_default_values(self): + """Test that RelatedUrl and AdditionalUrls use proper defaults when not provided""" + metadata = CheckMetadata( + Provider="aws", + CheckID="test_check_defaults", + CheckTitle="Test Check with Default Fields", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + # AdditionalUrls not provided - should default to empty list via default_factory + Compliance=[], + ) + + # Assert that the fields use their default values + assert metadata.RelatedUrl == "" # Should default to empty string + assert metadata.AdditionalUrls == [] # Should default to empty list + + def test_related_url_none_fails(self): + """Test that setting RelatedUrl to None raises a ValidationError""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata( + Provider="aws", + CheckID="test_check_none_related_url", + CheckTitle="Test Check with None RelatedUrl", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl=None, # This should fail + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=[], + Compliance=[], + ) + # Should contain a validation error for RelatedUrl + assert "RelatedUrl" in str(exc_info.value) + + def test_additional_urls_none_fails(self): + """Test that setting AdditionalUrls to None raises a ValidationError""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata( + Provider="aws", + CheckID="test_check_none_additional_urls", + CheckTitle="Test Check with None AdditionalUrls", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="https://example.com", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls=None, # This should fail + Compliance=[], + ) + # Should contain the validation error we set in the validator + assert "AdditionalUrls must be a list" in str(exc_info.value) + + def test_additional_urls_invalid_type_fails(self): + """Test that setting AdditionalUrls to non-list value raises a ValidationError""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata( + Provider="aws", + CheckID="test_check_invalid_additional_urls", + CheckTitle="Test Check with Invalid AdditionalUrls", + CheckType=["type1"], + ServiceName="test", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="https://example.com", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + AdditionalUrls="not_a_list", # This should fail + Compliance=[], + ) + # Should contain the validation error we set in the validator + assert "AdditionalUrls must be a list" in str(exc_info.value)