diff --git a/vulnerabilities/importer.py b/vulnerabilities/importer.py index 22e65a29b..36aed107c 100644 --- a/vulnerabilities/importer.py +++ b/vulnerabilities/importer.py @@ -41,7 +41,6 @@ from vulnerabilities.utils import get_reference_id from vulnerabilities.utils import is_cve from vulnerabilities.utils import nearest_patched_package -from vulnerabilities.models import VulnerabilityStatusType logger = logging.getLogger(__name__) @@ -249,7 +248,6 @@ class AdvisoryData: references: List[Reference] = dataclasses.field(default_factory=list) date_published: Optional[datetime.datetime] = None weaknesses: List[int] = dataclasses.field(default_factory=list) - status: int = dataclasses.field(default=VulnerabilityStatusType.PUBLISHED) def __post_init__(self): if self.date_published and not self.date_published.tzinfo: @@ -273,7 +271,6 @@ def to_dict(self): "references": [ref.to_dict() for ref in self.references], "date_published": self.date_published.isoformat() if self.date_published else None, "weaknesses": self.weaknesses, - "status": self.status, } @classmethod diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index 2a2426d9e..b84d562d2 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -8,7 +8,7 @@ # from vulnerabilities.improvers import valid_versions -from vulnerabilities.improvers import rejected_cves +from vulnerabilities.improvers import vulnerability_status IMPROVERS_REGISTRY = [ valid_versions.GitHubBasicImprover, @@ -23,7 +23,7 @@ valid_versions.IstioImprover, valid_versions.DebianOvalImprover, valid_versions.UbuntuOvalImprover, - rejected_cves.RejectedCvesImprover, + vulnerability_status.VulnerabilityStatusImprover, ] IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY} diff --git a/vulnerabilities/improvers/rejected_cves.py b/vulnerabilities/improvers/rejected_cves.py deleted file mode 100644 index 98c815325..000000000 --- a/vulnerabilities/improvers/rejected_cves.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Iterable -from vulnerabilities.importer import AdvisoryData -from vulnerabilities.improver import Improver, Inference -from django.db.models.query import QuerySet -from vulnerabilities.models import Advisory, Alias, Vulnerability - -class RejectedCvesImprover(Improver): - """ - Generate a translation of Advisory data - returned by the importers - into - full confidence inferences. These are basic database relationships for - unstructured data present in the Advisory model without any other - information source. - """ - - @property - def interesting_advisories(self) -> QuerySet: - return Advisory.objects.filter( - is_rejected = True - ) - - def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]: - if not advisory_data: - return [] - - aliases = advisory_data.aliases - aliases = Alias.objects.filter( - alias__in = aliases - ) - vulnerabilities = Vulnerability.objects.filter( - aliases__in = aliases - ).distinct() - - for vuln in vulnerabilities: - vuln.is_rejected = True - vuln.save() - return [] - diff --git a/vulnerabilities/improvers/vulnerability_status.py b/vulnerabilities/improvers/vulnerability_status.py new file mode 100644 index 000000000..52025d46d --- /dev/null +++ b/vulnerabilities/improvers/vulnerability_status.py @@ -0,0 +1,80 @@ +# +# 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/nexB/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +import urllib.parse +from typing import Iterable + +from django.db.models import Q +from django.db.models.query import QuerySet + +from vulnerabilities.importer import AdvisoryData +from vulnerabilities.importers.nvd import NVDImporter +from vulnerabilities.improver import Improver +from vulnerabilities.improver import Inference +from vulnerabilities.models import Advisory +from vulnerabilities.models import Alias +from vulnerabilities.models import Vulnerability +from vulnerabilities.models import VulnerabilityStatusType +from vulnerabilities.utils import fetch_response +from vulnerabilities.utils import get_item + +NVD_API_URL = "https://cveawg.mitre.org/api/cve/" + + +class VulnerabilityStatusImprover(Improver): + """ + Generate a translation of Advisory data - returned by the importers - into + full confidence inferences. These are basic database relationships for + unstructured data present in the Advisory model without any other + information source. + """ + + @property + def interesting_advisories(self) -> QuerySet: + return Advisory.objects.filter(Q(created_by=NVDImporter.qualified_name)).paginated() + + def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]: + if not advisory_data: + return [] + aliases = advisory_data.aliases + aliases = Alias.objects.filter(alias__in=aliases) + vulnerabilities = Vulnerability.objects.filter(aliases__in=aliases).distinct() + + cve_id = None + + for alias in aliases: + if alias.startswith("CVE"): + cve_id = alias + + if not cve_id: + return [] + + for vuln in vulnerabilities: + status = get_status_from_api(cve_id=cve_id) + if not status: + status = VulnerabilityStatusType.PUBLISHED + vuln.status = status + vuln.save() + return [] + + +def get_status_from_api(cve_id): + url = urllib.parse.urljoin(NVD_API_URL, cve_id) + try: + response = fetch_response(url=url) + except Exception as e: + return + response = response.json() + cve_state = get_item(response, "cveMetaData", "state") + tags = get_item(response, "containers", "cna", "tags") + if "disputed" in tags: + return VulnerabilityStatusType.DISPUTED + if cve_state == "REJECTED": + return VulnerabilityStatusType.REJECTED + return VulnerabilityStatusType.PUBLISHED diff --git a/vulnerabilities/migrations/0040_advisory_is_rejected_vulnerability_is_rejected.py b/vulnerabilities/migrations/0040_advisory_is_rejected_vulnerability_is_rejected.py deleted file mode 100644 index 07448095c..000000000 --- a/vulnerabilities/migrations/0040_advisory_is_rejected_vulnerability_is_rejected.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.7 on 2023-07-12 11:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("vulnerabilities", "0039_alter_vulnerabilityseverity_scoring_system"), - ] - - operations = [ - migrations.AddField( - model_name="advisory", - name="is_rejected", - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name="vulnerability", - name="is_rejected", - field=models.BooleanField(default=False), - ), - ] diff --git a/vulnerabilities/migrations/0041_advisory_status_vulnerability_status.py b/vulnerabilities/migrations/0041_advisory_status_vulnerability_status.py new file mode 100644 index 000000000..027539cc3 --- /dev/null +++ b/vulnerabilities/migrations/0041_advisory_status_vulnerability_status.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.7 on 2023-09-29 05:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0040_remove_advisory_date_improved_advisory_date_imported"), + ] + + operations = [ + migrations.AddField( + model_name="vulnerability", + name="status", + field=models.IntegerField( + choices=[(1, "published"), (2, "reserved"), (3, "disputed"), (4, "rejected")], + default=1, + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 2fbe5654a..52c4a600e 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -147,13 +147,15 @@ def with_package_counts(self): ), ) + class VulnerabilityStatusType(models.IntegerChoices): """List of vulnerability statuses.""" - PUBLISHED = 1, 'published' - RESERVED = 2, 'reserved' - DISPUTED = 3, 'disputed' - REJECTED = 4, 'rejected' + PUBLISHED = 1, "published" + RESERVED = 2, "reserved" + DISPUTED = 3, "disputed" + REJECTED = 4, "rejected" + class Vulnerability(models.Model): """ @@ -183,7 +185,9 @@ class Vulnerability(models.Model): through="PackageRelatedVulnerability", ) - status = models.IntegerField(max_length=100, choices=VulnerabilityStatusType.choices(), default=VulnerabilityStatusType.PUBLISHED) + status = models.IntegerField( + choices=VulnerabilityStatusType.choices, default=VulnerabilityStatusType.PUBLISHED + ) objects = VulnerabilityQuerySet.as_manager() @@ -841,7 +845,6 @@ class Advisory(models.Model): "module name importing the advisory. Eg:" "vulnerabilities.importers.nginx.NginxImporter", ) - status = models.IntegerField(choices=VulnerabilityStatusType, default=VulnerabilityStatusType.PUBLISHED) objects = AdvisoryQuerySet.as_manager() class Meta: @@ -860,6 +863,7 @@ def to_advisory_data(self) -> "AdvisoryData": from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackage from vulnerabilities.importer import Reference + return AdvisoryData( aliases=self.aliases, summary=self.summary, diff --git a/vulnerabilities/templates/0040_advisory_status_vulnerability_status.py b/vulnerabilities/templates/0040_advisory_status_vulnerability_status.py new file mode 100644 index 000000000..9ed4809fd --- /dev/null +++ b/vulnerabilities/templates/0040_advisory_status_vulnerability_status.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.7 on 2023-09-21 12:20 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0039_alter_vulnerabilityseverity_scoring_system"), + ] + + operations = [ + migrations.AddField( + model_name="advisory", + name="status", + field=models.IntegerField( + choices=[(1, "published"), (2, "reserved"), (3, "disputed"), (4, "rejected")], + default=1, + ), + ), + migrations.AddField( + model_name="vulnerability", + name="status", + field=models.IntegerField( + choices=[(1, "published"), (2, "reserved"), (3, "disputed"), (4, "rejected")], + default=1, + ), + ), + ] diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 9a27d16e4..deb16bb54 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -23,7 +23,7 @@ from vulnerabilities.forms import ApiUserCreationForm from vulnerabilities.forms import PackageSearchForm from vulnerabilities.forms import VulnerabilitySearchForm -from vulnerabilities.models import VulnerabilityStatusType, Weakness +from vulnerabilities.models import VulnerabilityStatusType from vulnerabilities.utils import get_severity_range from vulnerablecode.settings import env @@ -114,13 +114,13 @@ class VulnerabilityDetails(DetailView): def get_queryset(self): return super().get_queryset().prefetch_related("references", "aliases", "weaknesses") - + def get_status(self, status): status_by_keys = { - VulnerabilityStatus.PUBLISHED.name : "Published", - VulnerabilityStatus.REJECTED.name : "Rejected" + VulnerabilityStatusType.PUBLISHED: "Published", + VulnerabilityStatusType.REJECTED: "Rejected", + VulnerabilityStatusType.DISPUTED: "Disputed", } - print(status, status_by_keys) return status_by_keys[status] def get_context_data(self, **kwargs):