Skip to content

Commit 9c12d56

Browse files
committed
Add EPSS model
Add EPSS UI Add EPSS to api Fix api test Signed-off-by: ziadhany <[email protected]>
1 parent e36d9e1 commit 9c12d56

File tree

6 files changed

+228
-2
lines changed

6 files changed

+228
-2
lines changed

vulnerabilities/api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from rest_framework.throttling import AnonRateThrottle
2525
from rest_framework.throttling import UserRateThrottle
2626

27+
from vulnerabilities.models import EPSS
2728
from vulnerabilities.models import Alias
2829
from vulnerabilities.models import Package
2930
from vulnerabilities.models import Vulnerability
@@ -167,6 +168,12 @@ def to_representation(self, instance):
167168
return representation
168169

169170

171+
class EPSSSerializer(serializers.ModelSerializer):
172+
class Meta:
173+
model = EPSS
174+
fields = ["score", "percentile", "created_at", "updated_at"]
175+
176+
170177
class VulnerabilitySerializer(BaseResourceSerializer):
171178
fixed_packages = MinimalPackageSerializer(
172179
many=True, source="filtered_fixed_packages", read_only=True
@@ -175,6 +182,7 @@ class VulnerabilitySerializer(BaseResourceSerializer):
175182

176183
references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set")
177184
aliases = AliasSerializer(many=True, source="alias")
185+
epss = EPSSSerializer(read_only=True)
178186
weaknesses = WeaknessSerializer(many=True)
179187

180188
def to_representation(self, instance):
@@ -183,6 +191,10 @@ def to_representation(self, instance):
183191
weaknesses = data.get("weaknesses", [])
184192
data["weaknesses"] = [weakness for weakness in weaknesses if weakness is not None]
185193

194+
epss = data.get("epss", None)
195+
if not epss:
196+
data.pop("epss")
197+
186198
return data
187199

188200
class Meta:
@@ -196,6 +208,7 @@ class Meta:
196208
"affected_packages",
197209
"references",
198210
"weaknesses",
211+
"epss",
199212
]
200213

201214

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_epss
1112
from vulnerabilities.improvers import vulnerability_status
1213

1314
IMPROVERS_REGISTRY = [
@@ -26,6 +27,7 @@
2627
valid_versions.OSSFuzzImprover,
2728
valid_versions.RubyImprover,
2829
valid_versions.GithubOSVImprover,
30+
vulnerability_epss.EPSSImprover,
2931
vulnerability_status.VulnerabilityStatusImprover,
3032
]
3133

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
import csv
10+
import gzip
11+
import logging
12+
import urllib.request
13+
from datetime import datetime
14+
from typing import Iterable
15+
16+
from django.db.models.query import QuerySet
17+
18+
from vulnerabilities.importer import AdvisoryData
19+
from vulnerabilities.improver import Improver
20+
from vulnerabilities.improver import Inference
21+
from vulnerabilities.models import EPSS
22+
from vulnerabilities.models import Advisory
23+
from vulnerabilities.models import Alias
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
class EPSSImprover(Improver):
29+
"""Exploit Prediction Scoring System (EPSS) Improver"""
30+
31+
@property
32+
def interesting_advisories(self) -> QuerySet:
33+
return [Advisory.objects.all()[0]]
34+
35+
def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
36+
"""Fetch the csv epss file and update or create the EPSS Model"""
37+
38+
epss_url = "https://epss.cyentia.com/epss_scores-current.csv.gz"
39+
response = urllib.request.urlopen(epss_url)
40+
41+
with gzip.open(response, "rb") as f:
42+
lines = [l.decode("utf-8") for l in f.readlines()]
43+
44+
epss_reader = csv.reader(lines)
45+
next(epss_reader) # skip the header row
46+
47+
for epss_data in epss_reader:
48+
cve, score, percentile = epss_data
49+
alias = Alias.objects.get_or_none(alias=cve)
50+
if not alias:
51+
logger.error(f"Could not find alias for CVE: {cve}")
52+
continue
53+
54+
if not alias.vulnerability:
55+
logger.error(f"Could not find vulnerability with this CVE {cve}")
56+
continue
57+
58+
EPSS.objects.update_or_create(
59+
vulnerability=alias.vulnerability,
60+
defaults={
61+
"score": score,
62+
"percentile": percentile,
63+
"updated_at": datetime.now(),
64+
},
65+
)
66+
67+
return []
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Generated by Django 4.1.13 on 2024-05-19 21:37
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", "0056_alter_packagechangelog_software_version_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="EPSS",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
(
24+
"score",
25+
models.FloatField(
26+
help_text="the EPSS score representing the probability [0-1] of exploitation in the wild in the next 30 days"
27+
),
28+
),
29+
(
30+
"percentile",
31+
models.FloatField(
32+
verbose_name="the percentile of the current score,the proportion of all scored vulnerabilities with the same or a lower EPSS score"
33+
),
34+
),
35+
(
36+
"created_at",
37+
models.DateTimeField(
38+
auto_now_add=True, help_text="When was the first time we fetched epss"
39+
),
40+
),
41+
(
42+
"updated_at",
43+
models.DateTimeField(
44+
auto_now=True, help_text="When was the last time we fetched epss"
45+
),
46+
),
47+
(
48+
"vulnerability",
49+
models.OneToOneField(
50+
on_delete=django.db.models.deletion.CASCADE,
51+
related_name="epss",
52+
to="vulnerabilities.vulnerability",
53+
),
54+
),
55+
],
56+
),
57+
]

vulnerabilities/models.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,7 +1121,6 @@ class Meta:
11211121

11221122

11231123
class ChangeLog(models.Model):
1124-
11251124
action_time = models.DateTimeField(
11261125
# check if dates are actually UTC
11271126
default=timezone.now,
@@ -1261,7 +1260,6 @@ def log_action(self, package, action_type, actor_name, source_url, related_vulne
12611260

12621261

12631262
class PackageChangeLog(ChangeLog):
1264-
12651263
AFFECTED_BY = 1
12661264
FIXING = 2
12671265

@@ -1309,3 +1307,30 @@ def log_fixing(cls, package, importer, source_url, related_vulnerability):
13091307
source_url=source_url,
13101308
related_vulnerability=related_vulnerability,
13111309
)
1310+
1311+
1312+
class EPSS(models.Model):
1313+
"""
1314+
EPSS is a daily estimate of the probability of exploitation activity being observed over the next 30 days.
1315+
"""
1316+
1317+
vulnerability = models.OneToOneField(
1318+
Vulnerability, on_delete=models.CASCADE, related_name="epss"
1319+
)
1320+
1321+
score = models.FloatField(
1322+
help_text="the EPSS score representing the probability "
1323+
"[0-1] of exploitation in the wild in the next 30 days"
1324+
)
1325+
1326+
percentile = models.FloatField(
1327+
"the percentile of the current score,"
1328+
"the proportion of all scored vulnerabilities with the same or a lower EPSS score"
1329+
)
1330+
1331+
created_at = models.DateTimeField(
1332+
auto_now_add=True, help_text="When was the first time we fetched epss"
1333+
)
1334+
updated_at = models.DateTimeField(
1335+
auto_now=True, help_text="When was the last time we fetched epss"
1336+
)

vulnerabilities/templates/vulnerability_details.html

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@
6060
</span>
6161
</a>
6262
</li>
63+
{% if vulnerability.epss %}
64+
<li data-tab="epss">
65+
<a>
66+
<span>
67+
EPSS
68+
</span>
69+
</a>
70+
</li>
71+
{% endif %}
6372
<li data-tab="history">
6473
<a>
6574
<span>
@@ -374,6 +383,59 @@
374383
</tr>
375384
{% endfor %}
376385
</div>
386+
387+
{% if vulnerability.epss %}
388+
<div class="tab-div content" data-content="epss">
389+
<div class="has-text-weight-bold tab-nested-div ml-1 mb-1 mt-1">
390+
Exploit Prediction Scoring System
391+
</div>
392+
<table class="table vcio-table width-100-pct mt-2">
393+
<tbody>
394+
<tr>
395+
<td class="two-col-left">
396+
<span class="has-tooltip-multiline has-tooltip-black has-tooltip-arrow has-tooltip-text-left"
397+
data-tooltip="the percentile of the current score, the proportion of all scored vulnerabilities with the same or a lower EPSS score">
398+
Percentile:
399+
</span>
400+
</td>
401+
<td class="two-col-right">{{ vulnerability.epss.percentile }}</td>
402+
</tr>
403+
404+
<tr>
405+
<td class="two-col-left">
406+
<span class="has-tooltip-multiline has-tooltip-black has-tooltip-arrow has-tooltip-text-left"
407+
data-tooltip="the EPSS score representing the probability [0-1] of exploitation in the wild in the next 30 days (following score publication)">
408+
EPSS score:
409+
</span>
410+
</td>
411+
<td class="two-col-right">{{ vulnerability.epss.score }}</td>
412+
</tr>
413+
414+
<tr>
415+
<td class="two-col-left">
416+
<span
417+
class="has-tooltip-multiline has-tooltip-black has-tooltip-arrow has-tooltip-text-left"
418+
data-tooltip="When was the first time we fetched epss">
419+
Created at:
420+
</span>
421+
</td>
422+
<td class="two-col-right">{{ vulnerability.epss.created_at }}</td>
423+
</tr>
424+
425+
<tr>
426+
<td class="two-col-left">
427+
<span class="has-tooltip-multiline has-tooltip-black has-tooltip-arrow has-tooltip-text-left"
428+
data-tooltip="When was the last time we fetched epss">
429+
Updated at:
430+
</span>
431+
</td>
432+
<td class="two-col-right">{{ vulnerability.epss.updated_at }}</td>
433+
</tr>
434+
</tbody>
435+
</table>
436+
</div>
437+
{% endif %}
438+
377439
<div class="tab-div content" data-content="history">
378440
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
379441
<thead>

0 commit comments

Comments
 (0)