diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 58771c916..3f3024ffa 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -195,9 +195,24 @@ class Meta: ] def get_affected_by_vulnerabilities(self, obj): - return [vuln.vulnerability_id for vuln in obj.affected_by_vulnerabilities.all()] + """ + Return a dictionary with vulnerabilities as keys and their details, including fixed_by_packages. + """ + vulnerabilities = obj.affected_by_vulnerabilities.prefetch_related("fixed_by_packages") + result = {} + for vuln in vulnerabilities: + result[vuln.vulnerability_id] = { + "vulnerability_id": vuln.vulnerability_id, + "fixed_by_packages": [ + package.package_url for package in vuln.fixed_by_packages.all() + ], + } + return result def get_fixing_vulnerabilities(self, obj): + """ + Return a list of IDs of vulnerabilities that the package fixes. + """ return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()] diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index fd18fb28c..6b261b777 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -10,6 +10,7 @@ from vulnerabilities.improvers import valid_versions from vulnerabilities.improvers import vulnerability_status from vulnerabilities.pipelines import VulnerableCodePipeline +from vulnerabilities.pipelines import add_fixed_by_packages from vulnerabilities.pipelines import compute_package_risk from vulnerabilities.pipelines import enhance_with_exploitdb from vulnerabilities.pipelines import enhance_with_kev @@ -39,6 +40,7 @@ enhance_with_metasploit.MetasploitImproverPipeline, enhance_with_exploitdb.ExploitDBImproverPipeline, compute_package_risk.ComputePackageRiskPipeline, + add_fixed_by_packages.ComputeFixedByPackagesPipeline, ] IMPROVERS_REGISTRY = { diff --git a/vulnerabilities/migrations/0084_affectedbypackagerelatedvulnerability_fixed_by_packages.py b/vulnerabilities/migrations/0084_affectedbypackagerelatedvulnerability_fixed_by_packages.py new file mode 100644 index 000000000..6dc90d1ae --- /dev/null +++ b/vulnerabilities/migrations/0084_affectedbypackagerelatedvulnerability_fixed_by_packages.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.16 on 2024-11-20 07:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0083_alter_packagechangelog_software_version_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="affectedbypackagerelatedvulnerability", + name="fixed_by_packages", + field=models.ManyToManyField( + blank=True, + help_text="Packages that fix this vulnerability for\n the affected package.", + related_name="fixing_relationships", + to="vulnerabilities.package", + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index d6fae2408..f1456f457 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -1053,6 +1053,14 @@ class AffectedByPackageRelatedVulnerability(PackageRelatedVulnerabilityBase): related_name="affected_package_vulnerability_relations", ) + fixed_by_packages = models.ManyToManyField( + "Package", + blank=True, + related_name="fixing_relationships", + help_text="""Packages that fix this vulnerability for + the affected package.""", + ) + class Meta(PackageRelatedVulnerabilityBase.Meta): verbose_name_plural = "Affected By Package Related Vulnerabilities" diff --git a/vulnerabilities/pipelines/add_fixed_by_packages.py b/vulnerabilities/pipelines/add_fixed_by_packages.py new file mode 100644 index 000000000..9728d6827 --- /dev/null +++ b/vulnerabilities/pipelines/add_fixed_by_packages.py @@ -0,0 +1,70 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# +from aboutcode.pipeline import LoopProgress +from django.db import transaction + +from vulnerabilities.models import AffectedByPackageRelatedVulnerability +from vulnerabilities.models import FixingPackageRelatedVulnerability +from vulnerabilities.models import Package +from vulnerabilities.pipelines import VulnerableCodePipeline + + +class ComputeFixedByPackagesPipeline(VulnerableCodePipeline): + """ + Compute and populate the `fixed_by_packages` field in AffectedByPackageRelatedVulnerability. + + See https://github.com/aboutcode-org/vulnerablecode/issues/1543 + """ + + pipeline_id = "compute_fixed_by_packages" + license_expression = None + + @classmethod + def steps(cls): + return (cls.compute_and_store_fixed_by_packages,) + + def compute_and_store_fixed_by_packages(self): + affected_relationships = AffectedByPackageRelatedVulnerability.objects.all() + + self.log(f"Calculating `fixed_by_packages` for {affected_relationships.count():,d} records") + + progress = LoopProgress( + total_iterations=affected_relationships.count(), + logger=self.log, + progress_step=5, + ) + + updated_relationship_count = 0 + + for relationship in progress.iter(affected_relationships): + # Get fixing packages for this relationship + fixing_package_ids = FixingPackageRelatedVulnerability.objects.filter( + package__name=relationship.package.name, + package__type=relationship.package.type, + package__namespace=relationship.package.namespace, + vulnerability=relationship.vulnerability, + ).values_list("package__id", flat=True) + + # Update the ManyToMany field using the provided method + self.update_fixed_by_packages(relationship, fixing_package_ids) + updated_relationship_count += 1 + + self.log( + f"Successfully populated `fixed_by_packages` for {updated_relationship_count:,d} records" + ) + + @transaction.atomic + def update_fixed_by_packages(self, relationship, fixing_package_ids): + """ + Update the fixed_by_packages field for a given relationship. + """ + # Clear existing relations and add new ones + relationship.fixed_by_packages.clear() + packages = Package.objects.filter(id__in=fixing_package_ids) + relationship.fixed_by_packages.add(*packages)