From aae08ff708a501b203a4e81a30717298a6f52733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannik=20Tannh=C3=A4user?= Date: Fri, 23 Jan 2026 11:16:29 +0100 Subject: [PATCH] Add 'ignore_other_package_versions' flag If True, only signatures for the versions specified under 'package_version' are used. This includes all version-specific signatures and signatures listed under an affected entry with that version. In case no signatures exist for these versions for a certain CVE, all signatures are used instead. --- vanir/detector_common_flags.py | 23 +++++- vanir/detector_common_flags_test.py | 4 +- vanir/detector_runner.py | 4 +- vanir/refiner_test.py | 2 + vanir/scanners/scanner_base.py | 78 ++++++++++++++----- vanir/scanners/scanner_base_test.py | 3 + .../target_selection_strategy_test.py | 3 + vanir/sign_generator_runner_test.py | 2 + vanir/sign_generator_test.py | 2 + vanir/signature.py | 14 ++++ vanir/signature_test.py | 2 + vanir/testdata/test_signatures.py | 17 ++++ vanir/vulnerability.py | 3 + vanir/vulnerability_manager_test.py | 1 + 14 files changed, 135 insertions(+), 23 deletions(-) diff --git a/vanir/detector_common_flags.py b/vanir/detector_common_flags.py index 8d3f32c..301daa4 100644 --- a/vanir/detector_common_flags.py +++ b/vanir/detector_common_flags.py @@ -19,6 +19,7 @@ from absl import flags import dateutil.relativedelta +from vanir import vulnerability from vanir import vulnerability_manager from vanir import vulnerability_overwriter from vanir.scanners import scanner_base @@ -144,6 +145,14 @@ '(subjected to other flags). This flag can be specified multiple times.' ) +_IGNORE_OTHER_PACKAGE_VERSIONS = flags.DEFINE_bool( + 'ignore_other_package_versions', False, + 'If True, only signatures for the versions specified under package_version' + 'are used. This includes all version-specific signatures and signatures' + 'listed under an affected entry with that version. In case no signatures' + 'exist for these versions, all signatures are used instead.' +) + _OVERWRITE_SPECS = flags.DEFINE_string( 'overwrite_specs', None, @@ -330,6 +339,7 @@ def generate_vuln_manager_from_flags( def generate_finding_filters_from_flags( + vulnerabilities: Sequence[vulnerability.Vulnerability] ) -> Sequence[scanner_base.FindingsFilter]: """Parses flags related to finding filters and return the list of filters.""" filters = [] @@ -338,5 +348,16 @@ def generate_finding_filters_from_flags( scanner_base.PathPrefixFilter(path) for path in _IGNORE_SCAN_PATHS.value ) versions = _PACKAGE_VERSIONS.value if _PACKAGE_VERSIONS.value else [] - filters.append(scanner_base.PackageVersionSpecificSignatureFilter(versions)) + if not versions and _IGNORE_OTHER_PACKAGE_VERSIONS.value: + raise ValueError( + 'No versions specified in "package-versions",' + 'although ignore_other_package_versions is set to true.' + ) + filters.append( + scanner_base.PackageVersionSpecificSignatureFilter( + versions, + _IGNORE_OTHER_PACKAGE_VERSIONS.value, + vulnerabilities, + ) + ) return filters diff --git a/vanir/detector_common_flags_test.py b/vanir/detector_common_flags_test.py index cab8113..1a89377 100644 --- a/vanir/detector_common_flags_test.py +++ b/vanir/detector_common_flags_test.py @@ -228,7 +228,7 @@ def test_generate_vulnerability_filters_from_flags_ignores_low_severity(self): @flagsaver.flagsaver(ignore_scan_path=['path1', 'path2/3']) def test_generate_scan_path_finding_filters_from_flags(self): - filters = detector_common_flags.generate_finding_filters_from_flags() + filters = detector_common_flags.generate_finding_filters_from_flags([]) self.assertLen(filters, 3) self.assertIsInstance(filters[0], scanner_base.PathPrefixFilter) self.assertIsInstance(filters[1], scanner_base.PathPrefixFilter) @@ -238,7 +238,7 @@ def test_generate_scan_path_finding_filters_from_flags(self): @flagsaver.flagsaver(package_version=['1', '2']) def test_generate_version_finding_filters_from_flags(self): - filters = detector_common_flags.generate_finding_filters_from_flags() + filters = detector_common_flags.generate_finding_filters_from_flags([]) self.assertLen(filters, 1) self.assertIsInstance( filters[0], scanner_base.PackageVersionSpecificSignatureFilter diff --git a/vanir/detector_runner.py b/vanir/detector_runner.py index 7710525..00e1ff6 100644 --- a/vanir/detector_runner.py +++ b/vanir/detector_runner.py @@ -528,7 +528,9 @@ def main(argv: Sequence[str]) -> None: ) finding_filters = ( [scanner_base.ShortFunctionFilter()] - + list(detector_common_flags.generate_finding_filters_from_flags()) + + list(detector_common_flags.generate_finding_filters_from_flags( + vuln_manager.get_vulnerabilities()) + ) ) findings = scanner_base.ShortFunctionFilter().filter(findings) for finding_filter in finding_filters: diff --git a/vanir/refiner_test.py b/vanir/refiner_test.py index 9a68fe7..66b76fb 100644 --- a/vanir/refiner_test.py +++ b/vanir/refiner_test.py @@ -130,6 +130,7 @@ def setUp(self): exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, function_hash=self._hash(_TEST_NORMALIZED_FUNCTION_CODE), length=len(_TEST_NORMALIZED_FUNCTION_CODE), target_function='test_func1', @@ -152,6 +153,7 @@ def setUp(self): exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, line_hashes=test_line_hashes, threshold=0.9, ) diff --git a/vanir/scanners/scanner_base.py b/vanir/scanners/scanner_base.py index 008628a..3d97ef0 100644 --- a/vanir/scanners/scanner_base.py +++ b/vanir/scanners/scanner_base.py @@ -148,34 +148,74 @@ def filter(self, findings: Findings) -> Findings: class PackageVersionSpecificSignatureFilter(FindingsFilter): """Removes findings from version-specific signatures not matching given versions.""" - def __init__(self, versions: Collection[str]): + def __init__( + self, + versions: Collection[str], + ignore_other_package_versions: bool=False, + vulnerabilities: Sequence[vulnerability.Vulnerability]=[] + ): self._package_versions = frozenset(versions) + self._ignore_other_package_versions = ignore_other_package_versions + self._versions_per_CVE = self._get_package_versions_per_CVE(vulnerabilities) def filter(self, findings: Findings) -> Findings: filtered_findings = {} for sig, chunks in findings.items(): - # If the signature is not version-specific, keep. - if not sig.match_only_versions: - filtered_findings[sig] = chunks - continue - # If the signature's versions overlay with the package's versions, keep. - if set(sig.match_only_versions) & self._package_versions: - filtered_findings[sig] = chunks - continue - # If the signature has "X-next" listed and the package's version is newer - # than X, keep. Note that this versioning scheme is currently only used by - # Android; there are plans for a more generic approach in the future. - next_vers = [v for v in sig.match_only_versions if v.endswith('-next')] - # We are using string comparison for versioning; there is plan to - # incorporate OSV's SemVer comparison library in the future. + CVE_id = sig.signature_id.removesuffix('-' + sig.signature_id.split('-')[-1]) + + # Make sure there is at least one patch for the specified versions + # in case we want to ignore other package versions if ( - next_vers and - any(ver > min(next_vers) for ver in self._package_versions) + self._ignore_other_package_versions + and (self._versions_per_CVE[CVE_id] & self._package_versions) ): - filtered_findings[sig] = chunks - # Otherwise, filter out. + # Only keep signatures for a CVE that are in self._package_versions + if( + (set(sig.affected_entry_versions) & self._package_versions) or + (sig.match_only_versions and (set(sig.match_only_versions) & self._package_versions)) + ): + filtered_findings[sig] = chunks + else: + logging.debug(f'Ignoring {sig} due to version mismatch') + else: + # If the signature is not version-specific, keep. + if not sig.match_only_versions: + filtered_findings[sig] = chunks + continue + # If the signature's versions overlay with the package's versions, keep. + if set(sig.match_only_versions) & self._package_versions: + filtered_findings[sig] = chunks + continue + # If the signature has "X-next" listed and the package's version is newer + # than X, keep. Note that this versioning scheme is currently only used by + # Android; there are plans for a more generic approach in the future. + next_vers = [v for v in sig.match_only_versions if v.endswith('-next')] + # We are using string comparison for versioning; there is plan to + # incorporate OSV's SemVer comparison library in the future. + if ( + next_vers and + any(ver > min(next_vers) for ver in self._package_versions) + ): + filtered_findings[sig] = chunks + # Otherwise, filter out. return filtered_findings + def _get_package_versions_per_CVE( + self, + vulnerabilities: Sequence[vulnerability.Vulnerability] + ) -> Mapping[str, str]: + """Parse package version for all vulnerability affected entries.""" + mappings = {} + for vuln in vulnerabilities: + ids = [vuln.id] + if vuln.aliases: + ids.extend(vuln.aliases) + for vuln_id in ids: + mappings[vuln_id] = set() + for affected_entry in vuln.affected: + for vuln_id in ids: + mappings[vuln_id].update(affected_entry.versions) + return mappings @dataclasses.dataclass(frozen=True) class ScannedFileStats: diff --git a/vanir/scanners/scanner_base_test.py b/vanir/scanners/scanner_base_test.py index ad0be37..024bca1 100644 --- a/vanir/scanners/scanner_base_test.py +++ b/vanir/scanners/scanner_base_test.py @@ -39,6 +39,7 @@ def setUp(self): target_function='foo', length=3, truncated_path_level=None, + affected_entry_versions=None, exact_target_file_match_only=False, deprecated=False, match_only_versions=None, @@ -50,6 +51,7 @@ def setUp(self): source='https://android.googlesource.com/sign2_source', target_file='target.c', truncated_path_level=None, + affected_entry_versions=None, exact_target_file_match_only=False, deprecated=False, match_only_versions=None, @@ -60,6 +62,7 @@ def setUp(self): signature_id='sign3', target_file='target.h', truncated_path_level=None, + affected_entry_versions=None, exact_target_file_match_only=False, signature_version='v1', source='sign3_source', diff --git a/vanir/scanners/target_selection_strategy_test.py b/vanir/scanners/target_selection_strategy_test.py index c0cdb6e..3591bee 100644 --- a/vanir/scanners/target_selection_strategy_test.py +++ b/vanir/scanners/target_selection_strategy_test.py @@ -38,6 +38,7 @@ def setUp(self): target_file='exact_match1.c', target_function='foo', truncated_path_level=None, + affected_entry_versions=None, length=3, signature_version='v1', function_hash='func_hash', @@ -50,6 +51,7 @@ def setUp(self): source='https://android.googlesource.com/sign2_source', target_file='foo/exact_match2.c', truncated_path_level=None, + affected_entry_versions=None, signature_version='v1', deprecated=False, exact_target_file_match_only=False, @@ -64,6 +66,7 @@ def setUp(self): 'somewhat/different/dir/prefix/foo/bar/truncated_path_match.c' ), truncated_path_level=2, + affected_entry_versions=None, signature_version='v1', deprecated=False, exact_target_file_match_only=False, diff --git a/vanir/sign_generator_runner_test.py b/vanir/sign_generator_runner_test.py index afd65c8..8207aee 100644 --- a/vanir/sign_generator_runner_test.py +++ b/vanir/sign_generator_runner_test.py @@ -40,6 +40,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, line_hashes=[], threshold=0, ) @@ -52,6 +53,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, function_hash='func_hash', length=10, target_function='func', diff --git a/vanir/sign_generator_test.py b/vanir/sign_generator_test.py index d8ec758..3e04211 100644 --- a/vanir/sign_generator_test.py +++ b/vanir/sign_generator_test.py @@ -55,6 +55,7 @@ def setUp(self): exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, function_hash='12345', length=10, target_function='func', @@ -68,6 +69,7 @@ def setUp(self): exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, line_hashes=['12345'], threshold=0.5, ) diff --git a/vanir/signature.py b/vanir/signature.py index 684dac7..de9183b 100644 --- a/vanir/signature.py +++ b/vanir/signature.py @@ -149,6 +149,7 @@ class Signature(metaclass=abc.ABCMeta): to identify the target file. See the Truncated Path module for details. signature_id_prefix: Prepended to the signature hash to create the globally unique ID of the signature. If not given, signature_id will be invalid. + affected_entry_versions: Versions of the affected entry of the signature """ signature_id: str signature_version: str @@ -158,6 +159,7 @@ class Signature(metaclass=abc.ABCMeta): exact_target_file_match_only: bool match_only_versions: Optional[FrozenSet[str]] truncated_path_level: Optional[int] + affected_entry_versions: Optional[Sequence[str]] @property @abc.abstractmethod @@ -222,6 +224,11 @@ def from_osv_dict(cls, osv_dict: dict[str, Any]) -> Self: function_hash=int(osv_dict['digest']['function_hash']), length=int(osv_dict['digest']['length']), target_function=osv_dict['target']['function'], + affected_entry_versions=( + frozenset(osv_dict['affected_entry_versions']) + if 'affected_entry_versions' in osv_dict + else None + ), ) elif sig_type is SignatureType.LINE_SIGNATURE: sign = LineSignature( @@ -241,6 +248,11 @@ def from_osv_dict(cls, osv_dict: dict[str, Any]) -> Self: truncated_path_level=_get_truncated_path_level(osv_dict), line_hashes=[int(h) for h in osv_dict['digest']['line_hashes']], threshold=osv_dict['digest']['threshold'], + affected_entry_versions=( + frozenset(osv_dict['affected_entry_versions']) + if 'affected_entry_versions' in osv_dict + else None + ), ) else: raise ValueError(f'Signature type {sig_type} is unknown.') @@ -406,6 +418,7 @@ def create_from_function_chunk( function_hash=chunk.function_hash, length=len(chunk.normalized_code), target_function=chunk.base.name, + affected_entry_versions=None, ) def create_from_line_chunk( @@ -444,6 +457,7 @@ def create_from_line_chunk( truncated_path_level=truncated_path_level, line_hashes=chunk.line_hashes, threshold=containment_threshold, + affected_entry_versions=None, ) def add_used_signature_id(self, sig_id: str): diff --git a/vanir/signature_test.py b/vanir/signature_test.py index afcf0cd..77c9883 100644 --- a/vanir/signature_test.py +++ b/vanir/signature_test.py @@ -34,6 +34,7 @@ def setUp(self): exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, line_hashes=[123, 456], threshold=0.9, ) @@ -46,6 +47,7 @@ def setUp(self): exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, function_hash=11111, length=5, target_function='', diff --git a/vanir/testdata/test_signatures.py b/vanir/testdata/test_signatures.py index 5c1a2af..5248c50 100644 --- a/vanir/testdata/test_signatures.py +++ b/vanir/testdata/test_signatures.py @@ -18,6 +18,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=23171078215908543219710863598493879057, length=699, @@ -31,6 +32,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=102018818739139768709145498743568935218, length=623, @@ -44,6 +46,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=94912136455276258216923841845314585456, length=5803, @@ -57,6 +60,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, line_hashes=[ 4068369853657927194428980839065868322, @@ -116,6 +120,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, line_hashes=[ 169211069206287344238106283610653699511, @@ -137,6 +142,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=334410933136282710967774068023981460530, length=2763, @@ -150,6 +156,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=213724507498041325655797406518481755665, length=772, @@ -163,6 +170,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, line_hashes=[ 298009621754784947338976317639313641580, @@ -188,6 +196,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=334410933136282710967774068023981460530, length=2763, @@ -201,6 +210,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=213724507498041325655797406518481755665, length=772, @@ -214,6 +224,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, line_hashes=[ 298009621754784947338976317639313641580, @@ -239,6 +250,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=302779128589865924941927860846474042040, length=300, @@ -252,6 +264,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=86180056594490316600717451057640403042, length=1264, @@ -265,6 +278,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, line_hashes=[ 59279288054240772512122037148835217342, @@ -292,6 +306,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=22931789430284818538229315262645915337, length=1784, @@ -305,6 +320,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, function_hash=326322751240508323355186466492298396434, length=1337, @@ -318,6 +334,7 @@ exact_target_file_match_only=False, match_only_versions=None, truncated_path_level=None, + affected_entry_versions=None, deprecated=False, line_hashes=[ 264047575499705013826057957145778461627, diff --git a/vanir/vulnerability.py b/vanir/vulnerability.py index eed28e4..b238b35 100644 --- a/vanir/vulnerability.py +++ b/vanir/vulnerability.py @@ -87,6 +87,9 @@ def __init__( original_signatures = self.ecosystem_specific[OSV_VANIR_SIGNATURES] else: original_signatures = [] + for sig in original_signatures: + if not isinstance(sig, signature.Signature): + sig["affected_entry_versions"] = self.versions self.vanir_signatures = [ sig if isinstance(sig, signature.Signature) else signature.Signature.from_osv_dict(sig) diff --git a/vanir/vulnerability_manager_test.py b/vanir/vulnerability_manager_test.py index b88bbca..8cc3cdb 100644 --- a/vanir/vulnerability_manager_test.py +++ b/vanir/vulnerability_manager_test.py @@ -67,6 +67,7 @@ def setUp(self): truncated_path_level=self._test_osv_sign['target'].get( 'truncated_path_level' ), + affected_entry_versions=None, function_hash=self._test_osv_sign['digest']['function_hash'], length=self._test_osv_sign['digest']['length'], target_function=self._test_osv_sign['target']['function'],