diff --git a/metadata/dependency_metadata.py b/metadata/dependency_metadata.py index 40812be611..14e964240a 100644 --- a/metadata/dependency_metadata.py +++ b/metadata/dependency_metadata.py @@ -305,6 +305,10 @@ class DependencyMetadata: def name(self) -> Optional[str]: return self._return_as_property(known_fields.NAME) + @property + def mitigated(self) -> Optional[List[str]]: + return self._return_as_property(known_fields.MITIGATED) + @property def short_name(self) -> Optional[str]: return self._return_as_property(known_fields.SHORT_NAME) diff --git a/metadata/fields/custom/mitigated.py b/metadata/fields/custom/mitigated.py new file mode 100644 index 0000000000..f44c5cf9d2 --- /dev/null +++ b/metadata/fields/custom/mitigated.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# Copyright 2025 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 re +from typing import List, Optional, Tuple + +import metadata.fields.field_types as field_types +import metadata.fields.util as util +import metadata.validation_result as vr + +# List of supported vulnerability ID prefixes. +_VULN_PREFIXES = [ + "CVE", # Common Vulnerabilities and Exposures. + "GHSA", # GitHub Security Advisory. + "PYSEC", # Python Security Advisory. + "OSV", # Open Source Vulnerability. + "DSA", # Debian Security Advisory. +] + +_PREFIX_PATTERN = "|".join(_VULN_PREFIXES) +_VULN_ID_PATTERN = re.compile( + rf"^({_PREFIX_PATTERN})-[a-zA-Z0-9]{{4}}-[a-zA-Z0-9:-]+$") + + +def validate_vuln_ids(vuln_ids: str) -> Tuple[List[str], List[str]]: + """ + Validates a list of vulnerability identifiers and returns valid and invalid IDs. + + Supports multiple formats: + - CVE IDs (e.g., CVE-2024-12345) + - GitHub Security Advisories (e.g., GHSA-1234-5678-90ab) + - Python Security Advisories (e.g., PYSEC-2024-1234) + - Open Source Vulnerabilities (e.g., OSV-2024-1234) + - Debian Security Advisories (e.g., DSA-1234-1) + + Args: + vuln_ids: List of vulnerability identifiers to validate + + Returns: + Tuple of (valid_ids, invalid_ids) + """ + valid_vuln_ids = [] + invalid_vuln_ids = [] + + for cve in vuln_ids.split(","): + cve_stripped = cve.strip() + if _VULN_ID_PATTERN.match(cve_stripped): + valid_vuln_ids.append(cve_stripped) + else: + invalid_vuln_ids.append(cve) + + return valid_vuln_ids, invalid_vuln_ids + + +class MitigatedField(field_types.SingleLineTextField): + """Field for comma-separated vulnerability IDs.""" + + def __init__(self): + super().__init__(name="Mitigated") + + def validate(self, value: str) -> Optional[vr.ValidationResult]: + """Checks if the value contains valid CVE IDs.""" + if util.is_empty(value): + return None + _, invalid_vuln_ids = validate_vuln_ids(value) + + if invalid_vuln_ids: + return vr.ValidationWarning( + reason=f"{self._name} contains invalid vulnerability IDs.", + additional=[ + f"Invalid Vulnerability IDs: {util.quoted(invalid_vuln_ids)}", + "The following identifiers are supported: " + + ", ".join(_VULN_PREFIXES), + ], + ) + + return None + + def narrow_type(self, value: str) -> Optional[List[str]]: + if not value: + return None + vuln_ids, _ = validate_vuln_ids(value) + return vuln_ids diff --git a/metadata/fields/known.py b/metadata/fields/known.py index 6f97fccfcc..a0a64916a5 100644 --- a/metadata/fields/known.py +++ b/metadata/fields/known.py @@ -19,6 +19,7 @@ import metadata.fields.custom.date import metadata.fields.custom.license import metadata.fields.custom.license_file import metadata.fields.custom.local_modifications +import metadata.fields.custom.mitigated import metadata.fields.custom.url import metadata.fields.custom.version import metadata.fields.custom.revision @@ -46,6 +47,7 @@ VERSION = metadata.fields.custom.version.VersionField() REVISION = metadata.fields.custom.revision.RevisionField() LOCAL_MODIFICATIONS = metadata.fields.custom.local_modifications.LocalModificationsField( ) +MITIGATED = metadata.fields.custom.mitigated.MitigatedField() ALL_FIELDS = ( NAME, @@ -61,6 +63,7 @@ ALL_FIELDS = ( SHIPPED_IN_CHROMIUM, LICENSE_ANDROID_COMPATIBLE, CPE_PREFIX, + MITIGATED, DESCRIPTION, LOCAL_MODIFICATIONS, ) diff --git a/metadata/tests/fields_test.py b/metadata/tests/fields_test.py index 30f449309a..188d162746 100644 --- a/metadata/tests/fields_test.py +++ b/metadata/tests/fields_test.py @@ -18,6 +18,7 @@ sys.path.insert(0, _ROOT_DIR) import metadata.fields.known as known_fields import metadata.fields.field_types as field_types import metadata.validation_result as vr +import metadata.fields.custom.mitigated class FieldValidationTest(unittest.TestCase): @@ -232,5 +233,32 @@ class FieldValidationTest(unittest.TestCase): self.assertFalse( known_fields.LOCAL_MODIFICATIONS.should_terminate_field(value)) + def test_vulnerability_ids(self): + valid_ids = [ + "CVE-2024-12345", + "CVE-2024-1234567", + "PYSEC-2024-1234", + "OSV-2024-1234", + "DSA-1234-1", + "GHSA-1234-5678-90ab", + ] + + invalid_ids = [ + "CVE-123-456", + "GHSA-123-456", + "PYSEC-2024", # Missing ID part. + "NOT-A-VALID-ID", # Bad prefix. + "CVE_2024_12345", # Wrong separator. + "", # Empty. + " ", # Just space. + ] + + test_ids = valid_ids + invalid_ids + valid_result, invalid_result = metadata.fields.custom.mitigated.validate_vuln_ids( + ",".join(test_ids)) + + self.assertListEqual(sorted(valid_result), sorted(valid_ids)) + self.assertListEqual(sorted(invalid_result), sorted(invalid_ids)) + if __name__ == "__main__": unittest.main()