Skip to content

Commit dc94005

Browse files
authored
Mark advisories status according to NVD advisory (#1232)
* Mark advisories as rejected according to NVD advisory Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Add improver for marking vulnerabilities as rejected as per NVD Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Fix Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Fix vulnerability status improver Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Fix failing tests Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Change status rejected to invalid Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * fix tests Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Address review comments Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Fix failing tests Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> --------- Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent f167562 commit dc94005

14 files changed

Lines changed: 1234 additions & 882 deletions

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#
99

1010
from vulnerabilities.improvers import valid_versions
11+
from vulnerabilities.improvers import vulnerability_status
1112

1213
IMPROVERS_REGISTRY = [
1314
valid_versions.GitHubBasicImprover,
@@ -23,6 +24,7 @@
2324
valid_versions.DebianOvalImprover,
2425
valid_versions.UbuntuOvalImprover,
2526
valid_versions.OSSFuzzImprover,
27+
vulnerability_status.VulnerabilityStatusImprover,
2628
]
2729

2830
IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
11+
from typing import Iterable
12+
from urllib.parse import urljoin
13+
14+
from django.db.models import Q
15+
from django.db.models.query import QuerySet
16+
17+
from vulnerabilities.importer import AdvisoryData
18+
from vulnerabilities.importers.nvd import NVDImporter
19+
from vulnerabilities.improver import Improver
20+
from vulnerabilities.improver import Inference
21+
from vulnerabilities.models import Advisory
22+
from vulnerabilities.models import Alias
23+
from vulnerabilities.models import Vulnerability
24+
from vulnerabilities.models import VulnerabilityStatusType
25+
from vulnerabilities.utils import fetch_response
26+
from vulnerabilities.utils import get_item
27+
28+
MITRE_API_URL = "https://cveawg.mitre.org/api/cve/"
29+
30+
31+
class VulnerabilityStatusImprover(Improver):
32+
"""
33+
Update vulnerability with NVD statues
34+
"""
35+
36+
@property
37+
def interesting_advisories(self) -> QuerySet:
38+
return (
39+
Advisory.objects.filter(Q(created_by=NVDImporter.qualified_name))
40+
.distinct("aliases")
41+
.paginated()
42+
)
43+
44+
def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
45+
"""
46+
This is a work-around until we have new style importer and improver
47+
and this get_inferences function updates the vulnerability status directly
48+
# TODO: Replace this with new style improvers
49+
"""
50+
if not advisory_data:
51+
return []
52+
aliases = advisory_data.aliases
53+
# NVD Importer only has one alias in it and this a CVE
54+
assert len(aliases) == 1
55+
cve_id = aliases[0]
56+
if not cve_id.startswith("CVE"):
57+
return []
58+
59+
alias = Alias.objects.get(alias=cve_id)
60+
vulnerabilities = Vulnerability.objects.filter(aliases__alias=alias).distinct()
61+
62+
for vuln in vulnerabilities:
63+
status = get_status_from_api(cve_id=cve_id)
64+
if not status:
65+
status = VulnerabilityStatusType.PUBLISHED
66+
vuln.status = status
67+
vuln.save()
68+
return []
69+
70+
71+
def get_status_from_api(cve_id):
72+
"""
73+
Return the CVE status from the MITRE API
74+
"""
75+
url = urljoin(MITRE_API_URL, cve_id)
76+
try:
77+
response = fetch_response(url=url)
78+
except Exception as e:
79+
return
80+
response = response.json()
81+
cve_state = get_item(response, "cveMetadata", "state") or None
82+
tags = get_item(response, "containers", "cna", "tags") or []
83+
if "disputed" in tags:
84+
return VulnerabilityStatusType.DISPUTED
85+
if cve_state and cve_state == "REJECTED":
86+
return VulnerabilityStatusType.INVALID
87+
return VulnerabilityStatusType.PUBLISHED
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.1.7 on 2023-09-29 05:26
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0041_remove_vulns_with_empty_aliases"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="vulnerability",
15+
name="status",
16+
field=models.IntegerField(
17+
choices=[(1, "published"), (2, "disputed"), (3, "invalid")],
18+
default=1,
19+
),
20+
),
21+
]

vulnerabilities/models.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@
3333
from packageurl.contrib.django.models import without_empty_values
3434
from rest_framework.authtoken.models import Token
3535

36-
from vulnerabilities.importer import AdvisoryData
37-
from vulnerabilities.importer import AffectedPackage
38-
from vulnerabilities.importer import Reference
39-
from vulnerabilities.improver import MAX_CONFIDENCE
4036
from vulnerabilities.severity_systems import SCORING_SYSTEMS
4137
from vulnerabilities.utils import build_vcid
4238
from vulnerabilities.utils import remove_qualifiers_and_subpath
@@ -153,6 +149,14 @@ def with_package_counts(self):
153149
)
154150

155151

152+
class VulnerabilityStatusType(models.IntegerChoices):
153+
"""List of vulnerability statuses."""
154+
155+
PUBLISHED = 1, "Published"
156+
DISPUTED = 2, "Disputed"
157+
INVALID = 3, "Invalid"
158+
159+
156160
class Vulnerability(models.Model):
157161
"""
158162
A software vulnerability with a unique identifier and alternate ``aliases``.
@@ -181,6 +185,10 @@ class Vulnerability(models.Model):
181185
through="PackageRelatedVulnerability",
182186
)
183187

188+
status = models.IntegerField(
189+
choices=VulnerabilityStatusType.choices, default=VulnerabilityStatusType.PUBLISHED
190+
)
191+
184192
objects = VulnerabilityQuerySet.as_manager()
185193

186194
class Meta:
@@ -230,6 +238,11 @@ def get_aliases(self):
230238

231239
alias = get_aliases
232240

241+
@property
242+
def get_status_label(self):
243+
label_by_status = {choice[0]: choice[1] for choice in VulnerabilityStatusType.choices}
244+
return label_by_status.get(self.status) or VulnerabilityStatusType.PUBLISHED.label
245+
233246
def get_absolute_url(self):
234247
"""
235248
Return this Vulnerability details absolute URL.
@@ -653,6 +666,7 @@ class PackageRelatedVulnerability(models.Model):
653666
"module name responsible for creating this relation. Eg:"
654667
"vulnerabilities.importers.nginx.NginxBasicImprover",
655668
)
669+
from vulnerabilities.improver import MAX_CONFIDENCE
656670

657671
confidence = models.PositiveIntegerField(
658672
default=MAX_CONFIDENCE,
@@ -852,7 +866,11 @@ def save(self, *args, **kwargs):
852866
self.unique_content_id = checksum.hexdigest()
853867
super().save(*args, **kwargs)
854868

855-
def to_advisory_data(self) -> AdvisoryData:
869+
def to_advisory_data(self) -> "AdvisoryData":
870+
from vulnerabilities.importer import AdvisoryData
871+
from vulnerabilities.importer import AffectedPackage
872+
from vulnerabilities.importer import Reference
873+
856874
return AdvisoryData(
857875
aliases=self.aliases,
858876
summary=self.summary,

vulnerabilities/templates/vulnerability_details.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@
8888
</td>
8989
{% endif %}
9090
</tr>
91+
<tr>
92+
<td class="two-col-left">Status</td>
93+
<td class="two-col-right">{{ status }}</td>
94+
</tr>
9195
</tbody>
9296
</table>
9397
</div>

0 commit comments

Comments
 (0)