Skip to content

Commit 67dda33

Browse files
authored
Merge pull request #667 from TG1999/cves_from_cpe
Reference: #585 * Lookup Vulnerabilities by CPE
2 parents f87bf43 + 35c84e8 commit 67dda33

9 files changed

Lines changed: 175 additions & 9 deletions

File tree

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Version v30.0.0
1919

2020
- Add support for nix as a Linux deployemnet target.
2121

22+
- Lookup vulnerabilities from CPE
23+
2224

2325

2426
Version v20.10

vulnerabilities/api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,25 @@ class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet):
177177
paginate_by = 50
178178
filter_backends = (filters.DjangoFilterBackend,)
179179
filterset_class = VulnerabilityFilterSet
180+
181+
182+
class CPEFilterSet(filters.FilterSet):
183+
cpe = filters.CharFilter(method="filter_cpe")
184+
185+
class Meta:
186+
model = Vulnerability
187+
fields = ["vulnerabilityreference__reference_id"]
188+
189+
def filter_cpe(self, queryset, name, value):
190+
cpe = unquote(value)
191+
return self.queryset.filter(vulnerabilityreference__reference_id__startswith=cpe).distinct()
192+
193+
194+
class CPEViewSet(viewsets.ReadOnlyModelViewSet):
195+
queryset = Vulnerability.objects.filter(
196+
vulnerabilityreference__reference_id__startswith="cpe"
197+
).distinct()
198+
serializer_class = VulnerabilitySerializer
199+
paginate_by = 50
200+
filter_backends = (filters.DjangoFilterBackend,)
201+
filterset_class = CPEFilterSet

vulnerabilities/importers/nvd.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,12 @@ class NVDImporter(Importer):
4545
spdx_license_expression = "LicenseRef-scancode-unknown"
4646

4747
def advisory_data(self):
48-
advisory_data = []
4948
current_year = date.today().year
5049
# NVD json feeds start from 2002.
5150
for year in range(2002, current_year + 1):
5251
download_url = f"https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-{year}.json.gz"
5352
data = fetch(download_url)
54-
advisory_data.extend(to_advisories(data))
55-
return advisory_data
53+
yield from to_advisories(data)
5654

5755

5856
# Isolating network calls for simplicity of testing

vulnerabilities/improve_runner.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver
6363

6464
for ref in inference.references:
6565
reference, _ = models.VulnerabilityReference.objects.get_or_create(
66-
vulnerability=vuln, reference_id=ref.reference_id, url=ref.url
66+
reference_id=ref.reference_id, url=ref.url
67+
)
68+
69+
models.VulnerabilityRelatedReference.objects.update_or_create(
70+
reference=reference, vulnerability=vuln
6771
)
6872

6973
for severity in ref.severities:
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generated by Django 4.0.3 on 2022-04-22 15:43
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('vulnerabilities', '0009_alter_advisory_summary_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='VulnerabilityRelatedReference',
16+
fields=[
17+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
],
19+
),
20+
migrations.AlterUniqueTogether(
21+
name='vulnerabilityreference',
22+
unique_together={('url', 'reference_id')},
23+
),
24+
migrations.AddField(
25+
model_name='vulnerability',
26+
name='references',
27+
field=models.ManyToManyField(through='vulnerabilities.VulnerabilityRelatedReference', to='vulnerabilities.vulnerabilityreference'),
28+
),
29+
migrations.AddField(
30+
model_name='vulnerabilityreference',
31+
name='vulnerabilities',
32+
field=models.ManyToManyField(through='vulnerabilities.VulnerabilityRelatedReference', to='vulnerabilities.vulnerability'),
33+
),
34+
migrations.AddField(
35+
model_name='vulnerabilityrelatedreference',
36+
name='reference',
37+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.vulnerabilityreference'),
38+
),
39+
migrations.AddField(
40+
model_name='vulnerabilityrelatedreference',
41+
name='vulnerability',
42+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.vulnerability'),
43+
),
44+
migrations.RemoveField(
45+
model_name='vulnerabilityreference',
46+
name='vulnerability',
47+
),
48+
migrations.AlterUniqueTogether(
49+
name='vulnerabilityrelatedreference',
50+
unique_together={('vulnerability', 'reference')},
51+
),
52+
]

vulnerabilities/models.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ class Vulnerability(models.Model):
6060
blank=True,
6161
)
6262

63+
references = models.ManyToManyField(
64+
to="VulnerabilityReference", through="VulnerabilityRelatedReference"
65+
)
66+
6367
@property
6468
def vulcoid(self):
6569
return f"VULCOID-{self.vulnerability_id}"
@@ -92,10 +96,11 @@ class VulnerabilityReference(models.Model):
9296
package manager.
9397
"""
9498

95-
vulnerability = models.ForeignKey(
96-
Vulnerability,
97-
on_delete=models.CASCADE,
99+
vulnerabilities = models.ManyToManyField(
100+
to="Vulnerability",
101+
through="VulnerabilityRelatedReference",
98102
)
103+
99104
url = models.URLField(
100105
max_length=1024, help_text="URL to the vulnerability reference", blank=True
101106
)
@@ -111,7 +116,6 @@ def severities(self):
111116

112117
class Meta:
113118
unique_together = (
114-
"vulnerability",
115119
"url",
116120
"reference_id",
117121
)
@@ -121,6 +125,25 @@ def __str__(self):
121125
return f"{self.url}{reference_id}"
122126

123127

128+
class VulnerabilityRelatedReference(models.Model):
129+
"""
130+
A reference related to a vulnerability.
131+
"""
132+
133+
vulnerability = models.ForeignKey(
134+
Vulnerability,
135+
on_delete=models.CASCADE,
136+
)
137+
138+
reference = models.ForeignKey(
139+
VulnerabilityReference,
140+
on_delete=models.CASCADE,
141+
)
142+
143+
class Meta:
144+
unique_together = ("vulnerability", "reference")
145+
146+
124147
class Package(PackageURLMixin):
125148
"""
126149
A software package with related vulnerabilities.

vulnerabilities/tests/test_fix_api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
from vulnerabilities.models import Package
2727
from vulnerabilities.models import Vulnerability
28+
from vulnerabilities.models import VulnerabilityReference
29+
from vulnerabilities.models import VulnerabilityRelatedReference
2830

2931

3032
class APITestCaseVulnerability(TestCase):
@@ -85,3 +87,23 @@ def test_api_with_single_vulnerability(self):
8587
assert response["name"] == "test-vulnDB"
8688
assert response["version"] == "1.0"
8789
assert response["type"] == "generic"
90+
91+
92+
class CPEApi(TestCase):
93+
def setUp(self):
94+
self.vulnerability = Vulnerability.objects.create(summary="test")
95+
for i in range(0, 10):
96+
ref, _ = VulnerabilityReference.objects.get_or_create(
97+
reference_id=f"cpe:/a:nginx:{i}",
98+
)
99+
VulnerabilityRelatedReference.objects.create(
100+
reference=ref, vulnerability=self.vulnerability
101+
)
102+
103+
def test_api_status(self):
104+
response = self.client.get("/api/cpes/", format="json")
105+
self.assertEqual(status.HTTP_200_OK, response.status_code)
106+
107+
def test_api_response(self):
108+
response = self.client.get("/api/cpes/?cpe=cpe:/a:nginx:9", format="json").data
109+
self.assertEqual(response["count"], 1)

vulnerablecode/static/api_doc/api_schema.yaml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,47 @@ paths:
7777
schema:
7878
$ref: '#/components/schemas/Package'
7979
description: ''
80+
/api/cpes/:
81+
get:
82+
operationId: cpes_list
83+
description: ''
84+
tags:
85+
- cpe
86+
security:
87+
- cookieAuth: []
88+
- basicAuth: []
89+
- {}
90+
responses:
91+
'200':
92+
content:
93+
application/json:
94+
schema:
95+
$ref: '#/components/schemas/PaginatedPackageList'
96+
description: ''
97+
/api/cpes?cpe={cpe}:
98+
get:
99+
operationId: cpes_retrieve
100+
description: ''
101+
parameters:
102+
- in: path
103+
name: cpe
104+
schema:
105+
type: string
106+
description: A CPE, more info on CPE can be found here https://nvd.nist.gov/products/cpe
107+
required: true
108+
tags:
109+
- cpe
110+
security:
111+
- cookieAuth: []
112+
- basicAuth: []
113+
- {}
114+
responses:
115+
'200':
116+
content:
117+
application/json:
118+
schema:
119+
$ref: '#/components/schemas/Package'
120+
description: ''
80121
/api/packages/bulk_search/:
81122
post:
82123
operationId: packages_bulk_search
@@ -420,4 +461,4 @@ components:
420461
cookieAuth:
421462
type: apiKey
422463
in: cookie
423-
name: Session
464+
name: Session

vulnerablecode/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from django.urls import path
2727
from rest_framework.routers import DefaultRouter
2828

29+
from vulnerabilities.api import CPEViewSet
2930
from vulnerabilities.api import PackageViewSet
3031
from vulnerabilities.api import VulnerabilityViewSet
3132
from vulnerabilities.views import HomePage
@@ -47,6 +48,7 @@ def __init__(self, *args, **kwargs):
4748
api_router.register(r"packages", PackageViewSet)
4849
# `DefaultRouter` requires `basename` when registering viewsets that don't define a queryset.
4950
api_router.register(r"vulnerabilities", VulnerabilityViewSet, basename="vulnerability")
51+
api_router.register(r"cpes", CPEViewSet, basename="cpe")
5052

5153
urlpatterns = [
5254
path("admin/", admin.site.urls),

0 commit comments

Comments
 (0)