feat: Verify that the CheckID is the same as the filename and classname in the Check class (#8690)

Co-authored-by: angelolmn <e.angelolm#go.ugr.es>
Co-authored-by: César Arroba <cesar@prowler.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
OlmeNav
2025-09-30 13:46:59 +02:00
committed by GitHub
parent 250b5df836
commit 035293b612
3 changed files with 117 additions and 7 deletions
+1
View File
@@ -10,6 +10,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- LLM provider using `promptfoo` [(#8555)](https://github.com/prowler-cloud/prowler/pull/8555)
- Documentation for renaming checks [(#8717)](https://github.com/prowler-cloud/prowler/pull/8717)
- Add explicit "name" field for each compliance framework and include "FRAMEWORK" and "NAME" in CSV output [(#7920)](https://github.com/prowler-cloud/prowler/pull/7920)
- Equality validation for CheckID, filename and classname [(#8690)](https://github.com/prowler-cloud/prowler/pull/8690)
### Changed
+21 -6
View File
@@ -8,6 +8,7 @@ from enum import Enum
from typing import Any, Dict, Optional, Set
from pydantic.v1 import BaseModel, Field, ValidationError, validator
from pydantic.v1.error_wrappers import ErrorWrapper
from prowler.config.config import Provider
from prowler.lib.check.compliance_models import Compliance
@@ -444,17 +445,31 @@ class Check(ABC, CheckMetadata):
def __init__(self, **data):
"""Check's init function. Calls the CheckMetadataModel init."""
file_path = os.path.abspath(sys.modules[self.__module__].__file__)[:-3]
# Parse the Check's metadata file
metadata_file = (
os.path.abspath(sys.modules[self.__module__].__file__)[:-3]
+ ".metadata.json"
)
metadata_file = file_path + ".metadata.json"
# Store it to validate them with Pydantic
data = CheckMetadata.parse_file(metadata_file).dict()
# Calls parents init function
super().__init__(**data)
# TODO: verify that the CheckID is the same as the filename and classname
# to mimic the test done at test_<provider>_checks_metadata_is_valid
# Verify names consistency
check_id = self.CheckID
class_name = self.__class__.__name__
file_name = file_path.split(sep="/")[-1]
errors = []
if check_id != class_name:
errors.append(f"CheckID '{check_id}' != class name '{class_name}'")
if check_id != file_name:
errors.append(f"CheckID '{check_id}' != file name '{file_name}'")
if errors:
formatted_errors = [
ErrorWrapper(ValueError(err), loc=("CheckID",)) for err in errors
]
raise ValidationError(formatted_errors, model=CheckMetadata)
def metadata(self) -> dict:
"""Return the JSON representation of the check's metadata"""
+95 -1
View File
@@ -1,9 +1,10 @@
import sys
from unittest import mock
import pytest
from pydantic.v1 import ValidationError
from prowler.lib.check.models import CheckMetadata
from prowler.lib.check.models import Check, CheckMetadata
from tests.lib.check.compliance_check_test import custom_compliance_metadata
mock_metadata = CheckMetadata(
@@ -716,3 +717,96 @@ class TestCheckMetada:
)
# Should contain the validation error we set in the validator
assert "AdditionalURLs must be a list" 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):
"""Case where everything matches: CheckID == class_name == file_name"""
mock_parse_file.return_value = mock_metadata.copy(
update={
"CheckID": "accessanalyzer_enabled",
"ServiceName": "accessanalyzer",
}
)
class accessanalyzer_enabled(Check):
def execute(self):
pass
fake_module = mock.Mock()
fake_module.__file__ = "/path/to/accessanalyzer_enabled.py"
sys.modules[accessanalyzer_enabled.__module__] = fake_module
accessanalyzer_enabled()
@mock.patch("prowler.lib.check.models.CheckMetadata.parse_file")
def test_verify_names_consistency_class_mismatch(self, mock_parse_file):
"""CheckID != class name, but matches file_name"""
mock_parse_file.return_value = mock_metadata.copy(
update={
"CheckID": "accessanalyzer_enabled",
"ServiceName": "accessanalyzer",
}
)
class WrongClass(Check):
def execute(self):
pass
fake_module = mock.Mock()
fake_module.__file__ = "/path/to/accessanalyzer_enabled.py"
sys.modules[WrongClass.__module__] = fake_module
with pytest.raises(ValidationError) as excinfo:
WrongClass()
assert "!= class name" in str(excinfo.value)
@mock.patch("prowler.lib.check.models.CheckMetadata.parse_file")
def test_verify_names_consistency_file_mismatch(self, mock_parse_file):
"""CheckID == class name, but != file_name"""
mock_parse_file.return_value = mock_metadata.copy(
update={
"CheckID": "accessanalyzer_enabled",
"ServiceName": "accessanalyzer",
}
)
class accessanalyzer_enabled(Check):
def execute(self):
pass
fake_module = mock.Mock()
fake_module.__file__ = "/path/to/OtherFile.py"
sys.modules[accessanalyzer_enabled.__module__] = fake_module
with pytest.raises(ValidationError) as excinfo:
accessanalyzer_enabled()
assert "!= file name" in str(excinfo.value)
@mock.patch("prowler.lib.check.models.CheckMetadata.parse_file")
def test_verify_names_consistency_both_mismatch(self, mock_parse_file):
"""Neither class name nor file name match the CheckID"""
mock_parse_file.return_value = mock_metadata.copy(
update={
"CheckID": "accessanalyzer_enabled",
"ServiceName": "accessanalyzer",
}
)
class WrongClass(Check):
def execute(self):
pass
fake_module = mock.Mock()
fake_module.__file__ = "/path/to/OtherFile.py"
sys.modules[WrongClass.__module__] = fake_module
with pytest.raises(ValidationError) as excinfo:
WrongClass()
msg = str(excinfo.value)
assert "!= class name" in msg
assert "!= file name" in msg