diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index f15a884eb..309c2d838 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -30,6 +30,7 @@ from rest_framework.decorators import action from rest_framework.response import Response +from vulnerabilities.models import Alias from vulnerabilities.models import Package from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference @@ -47,7 +48,7 @@ class VulnerabilityReferenceSerializer(serializers.ModelSerializer): class Meta: model = VulnerabilityReference - fields = ["reference_id", "url", "scores"] + fields = ["url", "reference_id", "scores"] class MinimalPackageSerializer(serializers.HyperlinkedModelSerializer): @@ -74,18 +75,36 @@ class Meta: fields = ["url", "vulnerability_id", "references", "summary"] +class AliasSerializer(serializers.HyperlinkedModelSerializer): + """ + Used for nesting inside package focused APIs. + """ + + class Meta: + model = Alias + fields = ["alias"] + + class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer): resolved_packages = MinimalPackageSerializer(many=True, source="resolved_to", read_only=True) - unresolved_packages = MinimalPackageSerializer( - many=True, source="vulnerable_to", read_only=True - ) + affected_packages = MinimalPackageSerializer(many=True, source="vulnerable_to", read_only=True) references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set") + alias = AliasSerializer(many=True) class Meta: model = Vulnerability - fields = "__all__" + fields = [ + "url", + "vulcoid", + "summary", + "alias", + "vulnerability_id", + "resolved_packages", + "affected_packages", + "references", + ] class PackageSerializer(serializers.HyperlinkedModelSerializer): diff --git a/vulnerabilities/migrations/0010_vulnerability_packages_alter_package_vulnerabilities_and_more.py b/vulnerabilities/migrations/0010_vulnerability_packages_alter_package_vulnerabilities_and_more.py new file mode 100644 index 000000000..d86aa46f2 --- /dev/null +++ b/vulnerabilities/migrations/0010_vulnerability_packages_alter_package_vulnerabilities_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.3 on 2022-04-15 19:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0009_alter_advisory_summary_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='vulnerability', + name='packages', + field=models.ManyToManyField(through='vulnerabilities.PackageRelatedVulnerability', to='vulnerabilities.package'), + ), + migrations.AlterField( + model_name='package', + name='vulnerabilities', + field=models.ManyToManyField(through='vulnerabilities.PackageRelatedVulnerability', to='vulnerabilities.vulnerability'), + ), + migrations.AlterField( + model_name='packagerelatedvulnerability', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.package'), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index dee7773aa..d959f4b84 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -29,6 +29,7 @@ from django.core.validators import MaxValueValidator from django.core.validators import MinValueValidator from django.db import models +from django.utils.http import int_to_base36 from packageurl import PackageURL from packageurl.contrib.django.models import PackageURLMixin @@ -60,16 +61,21 @@ class Vulnerability(models.Model): blank=True, ) + packages = models.ManyToManyField( + to="Package", + through="PackageRelatedVulnerability", + ) + @property def vulcoid(self): - return f"VULCOID-{self.vulnerability_id}" + return f"VULCOID-{int_to_base36(self.id).upper()}" @property def vulnerable_to(self): """ Return packages that are vulnerable to this vulnerability. """ - return self.packages.filter(vulnerabilities__packagerelatedvulnerability__fix=False) + return self.packages.filter(packagerelatedvulnerability__fix=False) @property def resolved_to(self): @@ -77,7 +83,15 @@ def resolved_to(self): Returns packages that first received patch against this vulnerability in their particular version history. """ - return self.packages.filter(vulnerabilities__packagerelatedvulnerability__fix=True) + return self.packages.filter(packagerelatedvulnerability__fix=True) + + @property + def alias(self): + """ + Returns packages that first received patch against this vulnerability + in their particular version history. + """ + return self.aliases.all() def __str__(self): return self.vulcoid @@ -127,10 +141,7 @@ class Package(PackageURLMixin): """ vulnerabilities = models.ManyToManyField( - to="Vulnerability", - through="PackageRelatedVulnerability", - through_fields=("package", "vulnerability"), - related_name="packages", + to="Vulnerability", through="PackageRelatedVulnerability" ) # Remove the `qualifers` and `set_package_url` overrides after @@ -195,8 +206,14 @@ def __str__(self): class PackageRelatedVulnerability(models.Model): # TODO: Fix related_name - package = models.ForeignKey(Package, on_delete=models.CASCADE, related_name="package") - vulnerability = models.ForeignKey(Vulnerability, on_delete=models.CASCADE) + package = models.ForeignKey( + Package, + on_delete=models.CASCADE, + ) + vulnerability = models.ForeignKey( + Vulnerability, + on_delete=models.CASCADE, + ) created_by = models.CharField( max_length=100, blank=True, @@ -223,6 +240,10 @@ class Meta: verbose_name_plural = "PackageRelatedVulnerabilities" indexes = [models.Index(fields=["fix"])] + @property + def purl(self): + return self.package.package_url + def update_or_create(self): """ Update if supplied record has more confidence than existing record diff --git a/vulnerabilities/templates/package_update.html b/vulnerabilities/templates/package_update.html index 881b3ef84..b45afec74 100644 --- a/vulnerabilities/templates/package_update.html +++ b/vulnerabilities/templates/package_update.html @@ -30,7 +30,7 @@

-

Vulnerable To

+

Affected By

{% for vulnerability in impacted_vuln %} @@ -49,7 +49,7 @@

-

Safe To

+

Fixing

{% for vulnerability in resolved_vuln %} diff --git a/vulnerabilities/templates/vulnerability.html b/vulnerabilities/templates/vulnerability.html index 28563016d..9fa743b4c 100644 --- a/vulnerabilities/templates/vulnerability.html +++ b/vulnerabilities/templates/vulnerability.html @@ -16,6 +16,16 @@

Summary:

{% endif %}
+
+ {% if aliases %} +

Aliases:

+ {% for alias in aliases %} +

+ {{alias}} +

+ {% endfor %} + {% endif %} +
{% if object_list %} @@ -77,27 +87,51 @@

Severity

{% endif %} {% if vulnerability.vulnerable_to.all %} -

Vulnerable Packages

-
- {% for package in vulnerability.vulnerable_to.all %} - - {{package.package_url}} - - {% endfor %} -
+

Vulnerable Packages

+ + + + + {% for package in vulnerability.vulnerable_to.all %} + + {% if package.package_url %} + + + {% else %} + + + {% endif %} + + + {% endfor %} +
Packages
+ {{package.package_url}} + -
{% else %} -

No available vulnerable packages

+

No available Vulnerable packages

{% endif %} {% if vulnerability.resolved_to.all %}

Patched Packages

-
+ + + + {% for package in vulnerability.resolved_to.all %} - - {{package.package_url}} - + + {% if package.package_url %} + + + {% else %} + + + {% endif %} + + {% endfor %} - +
Packages
+ {{package.package_url}} + -
{% else %}

No available patched packages

{% endif %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index b9329978e..872544fc5 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -78,16 +78,18 @@ def request_to_queryset(request): .annotate( vulnerability_count=Count( "vulnerabilities", - filter=Q(vulnerabilities__packagerelatedvulnerability__fix=False), + filter=Q(packagerelatedvulnerability__fix=False), ), # TODO: consider renaming to fixed in the future patched_vulnerability_count=Count( "vulnerabilities", - filter=Q(vulnerabilities__packagerelatedvulnerability__fix=True), + filter=Q(packagerelatedvulnerability__fix=True), ), ) .prefetch_related() ) + # FIXME: This filter is wrong and ignoring most of the fields needed for a + # proper package lookup: type/namespace/name@version?qualifiers and so on class VulnerabilitySearchView(View): @@ -154,7 +156,9 @@ class VulnerabilityDetails(ListView): def get_context_data(self, **kwargs): context = super(VulnerabilityDetails, self).get_context_data(**kwargs) - context["vulnerability"] = models.Vulnerability.objects.get(id=self.kwargs["pk"]) + vulnerability = models.Vulnerability.objects.get(id=self.kwargs["pk"]) + context["vulnerability"] = vulnerability + context["aliases"] = vulnerability.aliases.alias() return context def get_queryset(self): diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index a1beb1631..a8f8a3498 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -22,8 +22,10 @@ # Visit https://github.com/nexB/vulnerablecode/ for support and download. from django.contrib import admin +from django.shortcuts import redirect from django.urls import include from django.urls import path +from django.views.generic import RedirectView from rest_framework.routers import DefaultRouter from vulnerabilities.api import PackageViewSet @@ -49,12 +51,12 @@ def __init__(self, *args, **kwargs): api_router.register(r"vulnerabilities", VulnerabilityViewSet, basename="vulnerability") urlpatterns = [ - path("admin/", admin.site.urls), + # path("admin/", admin.site.urls), path("api/docs", schema_view, name="redoc"), path("packages/search", PackageSearchView.as_view(), name="package_search"), path("packages/", PackageUpdate.as_view(), name="package_view"), path("vulnerabilities/", VulnerabilityDetails.as_view(), name="vulnerability_view"), path("vulnerabilities/search", VulnerabilitySearchView.as_view(), name="vulnerability_search"), - path("", HomePage.as_view(), name="home"), + path("", RedirectView.as_view(url="/api/docs"), name="home"), path(r"api/", include(api_router.urls)), ]