#!/usr/bin/env vpython3 # Copyright (c) 2023 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import os import sys import unittest _THIS_DIR = os.path.abspath(os.path.dirname(__file__)) # The repo's root directory. _ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", "..")) # Add the repo's root directory for clearer imports. sys.path.insert(0, _ROOT_DIR) import metadata.dependency_metadata as dm import metadata.fields.known as known_fields import metadata.validation_result as vr class DependencyValidationTest(unittest.TestCase): def test_repeated_field(self): """Check that a validation error is returned for a repeated field. """ dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test repeated field") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") dependency.add_entry(known_fields.NAME.get_name(), "again") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertTrue(isinstance(results[0], vr.ValidationError)) self.assertEqual(results[0].get_reason(), "There is a repeated field.") def test_only_alias_field(self): """Check that an alias field can be used for a main field.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.NAME.get_name(), "Test alias field used") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") # Use Shipped in Chromium instead of Shipped. dependency.add_entry(known_fields.SHIPPED_IN_CHROMIUM.get_name(), "no") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 0) def test_alias_overwrite_invalid_field(self): """Check that the value for an overwritten field (from an alias field) is still validated. """ dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.NAME.get_name(), "Test alias field overwrite") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.SHIPPED_IN_CHROMIUM.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "test") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertEqual(results[0].get_reason(), "Shipped is invalid.") def test_alias_invalid_field_attributed(self): """Check that an invalid value from an alias field is attributed to that alias field. """ dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.NAME.get_name(), "Test alias field error attributed") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.SHIPPED_IN_CHROMIUM.get_name(), "test") dependency.add_entry(known_fields.SHIPPED.get_name(), "yes") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertEqual(results[0].get_reason(), "Shipped in Chromium is invalid.") def test_versioning_field(self): """Check that a validation error is returned for insufficient versioning info. No Date/Revision and Version is N/A.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test metadata missing versioning info") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "N/A") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertTrue(isinstance(results[0], vr.ValidationError)) self.assertEqual(results[0].get_reason(), "Versioning fields are insufficient.") def test_versioning_with_invalid_revision(self): """Check that a validation error is returned for insufficient versioning info.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test metadata missing versioning info") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "N/A") dependency.add_entry(known_fields.REVISION.get_name(), "N/A") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 2) self.assertTrue(isinstance(results[0], vr.ValidationError)) self.assertTrue(isinstance(results[1], vr.ValidationError)) self.assertEqual(results[0].get_reason(), "Revision is not a valid hexadecimal revision.") self.assertEqual(results[1].get_reason(), "Versioning fields are insufficient.") def test_invalid_revision(self): """Check invalid revision formats return validation errors.""" # Test invalid revision format (non-hexadecimal). dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Invalid Revision") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.REVISION.get_name(), "invalid_revision") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertTrue(isinstance(results[0], vr.ValidationError)) self.assertEqual( results[0].get_reason(), "Revision is not a valid hexadecimal revision.", ) def test_valid_revision(self): """Check valid revision formats return no validation issues.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Valid Revision") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) # No errors for no revision. self.assertEqual(len(results), 0) dependency.add_entry(known_fields.REVISION.get_name(), "abcdef1") # Valid. results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) # No errors for valid revision. self.assertEqual(len(results), 0) def test_valid_revision_in_deps(self): """Check "Revision: DEPS" is acceptable.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Dependency") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "N/A") dependency.add_entry(known_fields.REVISION.get_name(), "DEPS") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) # No errors for no revision. self.assertEqual(len(results), 0) def test_required_field(self): """Check that a validation error is returned for a missing field.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.SHIPPED.get_name(), "no") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.NAME.get_name(), "Test missing field") # Leave URL field unspecified. results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertTrue(isinstance(results[0], vr.ValidationError)) self.assertEqual(results[0].get_reason(), "Required field 'URL' is missing.") def test_invalid_field(self): """Check field validation issues are returned.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.NAME.get_name(), "Test invalid field") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "test") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertTrue(isinstance(results[0], vr.ValidationError)) self.assertEqual(results[0].get_reason(), "Security Critical is invalid.") def test_invalid_license_file_path(self): """Check license file path validation issues are returned.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test license file path") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "MISSING-LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 1) self.assertTrue(isinstance(results[0], vr.ValidationWarning)) self.assertEqual(results[0].get_reason(), "License File is invalid.") def test_multiple_validation_issues(self): """Check all validation issues are returned.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test multiple errors") # Leave URL field unspecified. dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "MISSING-LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "test") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") dependency.add_entry(known_fields.NAME.get_name(), "again") # Check 4 validation results are returned, for: # - missing field; # - invalid license file path; # - invalid yes/no field value; and # - repeated field entry. results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 4) def test_valid_metadata(self): """Check valid metadata returns no validation issues.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test valid metadata") dependency.add_entry(known_fields.URL.get_name(), "https://www.example.com") dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no") dependency.add_entry(known_fields.SHIPPED.get_name(), "no") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 0) def test_dep_is_canonical_skips_versioning_requirement(self): """ Check versioning information isn't required for dependencies where Chromium is the canonical repository. """ dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test valid metadata") dependency.add_entry(known_fields.URL.get_name(), "This is the canonical repository") dependency.add_entry(known_fields.VERSION.get_name(), "N/A") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "yes") dependency.add_entry(known_fields.SHIPPED.get_name(), "yes") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) self.assertEqual(len(results), 0) def test_all_licenses_allowlisted(self): """Test that a single allowlisted license returns True.""" dependency = dm.DependencyMetadata() self.assertTrue(dependency.all_licenses_allowlisted("MIT", False)) self.assertTrue(dependency.all_licenses_allowlisted("MIT, GPL-2.0", False)) self.assertTrue(dependency.all_licenses_allowlisted("MIT, Apache-2.0", False)) self.assertFalse(dependency.all_licenses_allowlisted("InvalidLicense", False)) self.assertFalse(dependency.all_licenses_allowlisted("MIT, InvalidLicense", False)) self.assertFalse(dependency.all_licenses_allowlisted("", False)) # "MPL-2.0" is a reciprocal license, i.e. only allowed in open source projects. self.assertTrue(dependency.all_licenses_allowlisted("MPL-2.0", True)) self.assertFalse(dependency.all_licenses_allowlisted("MPL-2.0", False)) # Restricted licenses are treated the same as other license types, until # the exception and enforcement is resourced. self.assertTrue(dependency.all_licenses_allowlisted("GPL-2.0", False)) self.assertTrue(dependency.all_licenses_allowlisted("GPL-2.0", True)) self.assertFalse(dependency.all_licenses_allowlisted("MPL-2.0, GPL-2.0", False)) def test_only_open_source_licenses(self): """Test that only open source licenses are returned.""" dependency = dm.DependencyMetadata() self.assertEqual(dependency.only_open_source_licenses(""), []) self.assertEqual(dependency.only_open_source_licenses("MIT"), []) self.assertEqual(dependency.only_open_source_licenses("GPL-2.0"), []) self.assertEqual(dependency.only_open_source_licenses("MPL-2.0"), ["MPL-2.0"]) result = dependency.only_open_source_licenses("MIT, MPL-2.0") self.assertEqual(result, ["MPL-2.0"]) result = dependency.only_open_source_licenses("MPL-2.0, APSL-2.0") self.assertEqual(set(result), {"MPL-2.0", "APSL-2.0"}) # Test with mix of invalid and valid licenses result = dependency.only_open_source_licenses("InvalidLicense, MPL-2.0") self.assertEqual(result, ["MPL-2.0"]) def test_mitigated_validation(self): """Tests validation of Mitigated field and corresponding CVE descriptions.""" dependency = dm.DependencyMetadata() dependency.add_entry(known_fields.NAME.get_name(), "Test Dependency") dependency.add_entry(known_fields.URL.get_name(), "http://example.com") dependency.add_entry(known_fields.VERSION.get_name(), "1.0") dependency.add_entry(known_fields.LICENSE.get_name(), "MIT") dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE") dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "yes") dependency.add_entry(known_fields.SHIPPED.get_name(), "yes") # Add description for one CVE and an extra one. dependency.add_entry("CVE-2024-1234", "Fixed in this version") dependency.add_entry("CVE-2024-9999", "This shouldn't be here") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) # Check that a warning is returned when only CVE descriptions are # present. self.assertEqual(len(results), 1) self.assertTrue(isinstance(results[0], vr.ValidationWarning)) self.assertEqual(results[0].get_reason(), "Found descriptions for unlisted vulnerability IDs") self.assertIn("CVE-2024-1234",results[0].get_additional()[0]) self.assertIn("CVE-2024-9999",results[0].get_additional()[0]) # Add Mitigated field with two CVEs. dependency.add_entry(known_fields.MITIGATED.get_name(), "CVE-2024-1234, CVE-2024-5678") results = dependency.validate( source_file_dir=os.path.join(_THIS_DIR, "data"), repo_root_dir=_THIS_DIR, ) # Should get two warnings: # 1. Missing description for CVE-2024-5678 # 2. Extra description for CVE-2024-9999 self.assertEqual(len(results), 2) self.assertTrue(isinstance(results[0], vr.ValidationWarning)) self.assertEqual(results[0].get_reason(), "Missing descriptions for vulnerability IDs") self.assertIn("CVE-2024-5678",results[0].get_additional()[0]) self.assertTrue(isinstance(results[1], vr.ValidationWarning)) self.assertEqual(results[1].get_reason(), "Found descriptions for unlisted vulnerability IDs") self.assertIn("CVE-2024-9999",results[1].get_additional()[0]) if __name__ == "__main__": unittest.main()