from pathlib import Path from unittest.mock import patch, PropertyMock import pytest from rspec_tools.errors import RuleValidationError from copy import deepcopy from rspec_tools.rules import LanguageSpecificRule, RulesRepository from rspec_tools.validation.metadata import validate_rule_specialization_metadata, validate_rule_metadata @pytest.fixture def rule_language(mockrules: Path): rule = RulesRepository(rules_path=mockrules).get_rule('S100') return rule.get_language('kotlin') @pytest.fixture def invalid_rules(): invalid_rules_path = Path(__file__).parent.parent.joinpath('resources', 'invalid-rules') return RulesRepository(rules_path=invalid_rules_path) def test_valid_metadata_passes_validation(rule_language: LanguageSpecificRule): '''Check that language metadata are correctly overridden.''' validate_rule_specialization_metadata(rule_language) @patch('rspec_tools.validation.metadata.RULES_WITH_NO_LANGUAGES', []) def test_rule_with_no_language(invalid_rules: RulesRepository): s501 = invalid_rules.get_rule('S501') with pytest.raises(RuleValidationError, match=fr'^Rule S501 has no language-specific data'): validate_rule_metadata(s501) @patch('rspec_tools.validation.metadata.RULES_WITH_NO_LANGUAGES', ['S501']) def test_rule_with_no_language_in_exception_list(invalid_rules: RulesRepository): s501 = invalid_rules.get_rule('S501') validate_rule_metadata(s501) with patch.dict(s501.generic_metadata, [('status', 'deprecated')]): validate_rule_metadata(s501) @patch('rspec_tools.validation.metadata.RULES_WITH_NO_LANGUAGES', ['S501']) def test_open_rule_with_no_language_in_exception_list(invalid_rules: RulesRepository): s501 = invalid_rules.get_rule('S501') with pytest.raises(RuleValidationError, match=fr'^Rule S501 should be closed or deprecated'): with patch.dict(s501.generic_metadata, [('status', 'ready')]): validate_rule_metadata(s501) @patch('rspec_tools.validation.metadata.RULES_WITH_NO_LANGUAGES', ['S120']) def test_rule_expected_to_have_no_language(mockrules: Path): valid_rule = RulesRepository(rules_path=mockrules).get_rule('S120') with pytest.raises(RuleValidationError, match=fr'^Rule S120 should have no specializations'): validate_rule_metadata(valid_rule) @patch('rspec_tools.validation.metadata.RULES_WITH_NO_LANGUAGES', []) def test_rule_with_invalid_language(invalid_rules: RulesRepository): s502 = invalid_rules.get_rule('S502') with pytest.raises(RuleValidationError, match=fr'^Rule S502 failed validation for these reasons:\n - Rule scala:S502 has invalid metadata'): validate_rule_metadata(s502) def test_rule_that_is_fully_valid(mockrules: Path): valid_rule = RulesRepository(rules_path=mockrules).get_rule('S120') validate_rule_metadata(valid_rule) def test_missing_required_property_fails_validation(rule_language: LanguageSpecificRule): invalid_metadata = deepcopy(rule_language.metadata) del invalid_metadata['title'] with pytest.raises(RuleValidationError, match=fr'^Rule {rule_language.id} has invalid metadata'): with patch.object(LanguageSpecificRule, 'metadata', new_callable=PropertyMock) as mock: mock.return_value = invalid_metadata validate_rule_specialization_metadata(rule_language) def test_invalid_remediation_fails_validation(rule_language: LanguageSpecificRule): invalid_metadata = deepcopy(rule_language.metadata) invalid_metadata['remediation']["func"] = 42 with pytest.raises(RuleValidationError, match=fr'^Rule {rule_language.id} has invalid metadata'): with patch.object(LanguageSpecificRule, 'metadata', new_callable=PropertyMock) as mock: mock.return_value = invalid_metadata validate_rule_specialization_metadata(rule_language) def test_adding_properties_fails_validation(rule_language: LanguageSpecificRule): metadata = deepcopy(rule_language.metadata) metadata['unknown'] = 42 with pytest.raises(RuleValidationError, match=fr'^Rule {rule_language.id} has invalid metadata'): with patch.object(LanguageSpecificRule, 'metadata', new_callable=PropertyMock) as mock: mock.return_value = metadata validate_rule_specialization_metadata(rule_language) def test_ready_rule_with_replacement_fails_validation(rule_language: LanguageSpecificRule): invalid_metadata = deepcopy(rule_language.metadata) invalid_metadata['extra'] = { 'replacementRules': [ 'RSPEC-1234', 'RSPEC-5678' ]} with pytest.raises(RuleValidationError, match=fr'^Rule {rule_language.id} has invalid metadata: status'): with patch.object(LanguageSpecificRule, 'metadata', new_callable=PropertyMock) as mock: mock.return_value = invalid_metadata validate_rule_specialization_metadata(rule_language) def test_deprecated_rule_with_replacement_passes_validation(rule_language: LanguageSpecificRule): metadata = deepcopy(rule_language.metadata) metadata['extra'] = { 'replacementRules': [ 'RSPEC-1234' ]} metadata['status'] = 'deprecated' with patch.object(LanguageSpecificRule, 'metadata', new_callable=PropertyMock) as mock: mock.return_value = metadata validate_rule_specialization_metadata(rule_language) def test_rule_with_incomplete_list_of_security_standard_fails_validation(rule_language: LanguageSpecificRule): invalid_metadata = deepcopy(rule_language.metadata) # "OWASP Top 10 2021", defined in the generic metadata is missing invalid_metadata['securityStandards'] = {'ASVS 4': [], 'OWASP': [], 'CERT': []} with pytest.raises(RuleValidationError, match=fr'^Rule {rule_language.id} has invalid metadata: securityStandard'): with patch.object(LanguageSpecificRule, 'metadata', new_callable=PropertyMock) as mock: mock.return_value = invalid_metadata validate_rule_specialization_metadata(rule_language) def test_rule_with_complete_list_of_security_standard_passes_validation(rule_language: LanguageSpecificRule): metadata = deepcopy(rule_language.metadata) metadata['securityStandards'] = {'ASVS 4': [], 'OWASP': [], "OWASP Top 10 2021": []} with patch.object(LanguageSpecificRule, 'metadata', new_callable=PropertyMock) as mock: mock.return_value = metadata validate_rule_specialization_metadata(rule_language)