Skip to content

Commit 1412279

Browse files
singhdaksh7dakshsingh97
authored andcommitted
feat: add EPSS history storage and trend chart visualization
- Fetch full EPSS score history ordered by published_at in VulnerabilityDetails view - Serialize EPSS history as JSON and pass to template context - Add Chart.js line chart in EPSS tab to visualize score trend over time - Add EPSS score + percentile row to Essentials tab severity table Closes #1276 Relates to #1732 Signed-off-by: Daksh Singh <manavdaksh7@gmail.com>
1 parent 74172c4 commit 1412279

File tree

2 files changed

+97
-6
lines changed

2 files changed

+97
-6
lines changed

vulnerabilities/templates/vulnerability_details.html

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@
152152
{{ vulnerability.risk_score }}
153153
</td>
154154
</tr>
155+
{% if epss_data %}
156+
<tr>
157+
<td class="two-col-left">
158+
<span class="has-tooltip-multiline has-tooltip-black has-tooltip-arrow has-tooltip-text-left"
159+
data-tooltip="The EPSS score represents the probability [0-1] of exploitation in the wild in the next 30 days. Percentile indicates the proportion of all scored vulnerabilities with the same or a lower score.">
160+
EPSS Score
161+
</span>
162+
</td>
163+
<td class="two-col-right wrap-strings">
164+
{{ epss_data.score }}
165+
<span class="has-text-grey ml-2">({{ epss_data.percentile }} percentile)</span>
166+
</td>
167+
</tr>
168+
{% endif %}
155169
<tr>
156170
<td class="two-col-left"
157171
data-tooltip="Risk expressed as a number ranging from 0 to 10. It is calculated by multiplying
@@ -502,7 +516,7 @@
502516
{% endfor %}
503517
</div>
504518

505-
519+
506520
<div class="tab-div content" data-content="epss">
507521
{% if epss_data %}
508522
<div class="has-text-weight-bold tab-nested-div ml-1 mb-1 mt-1">
@@ -541,6 +555,14 @@
541555
{% endif %}
542556
</tbody>
543557
</table>
558+
559+
<div class="has-text-weight-bold tab-nested-div ml-1 mb-1 mt-4">
560+
EPSS Score Trend
561+
</div>
562+
<div style="max-width: 800px; margin-top: 1rem;">
563+
<canvas id="epssChart"></canvas>
564+
</div>
565+
<script id="epss-history-data" type="application/json">{{ epss_history_json }}</script>
544566
{% else %}
545567
<p>No EPSS data available for this vulnerability.</p>
546568
{% endif %}
@@ -597,6 +619,58 @@
597619
{% endif %}
598620

599621
<script src="{% static 'js/main.js' %}" crossorigin="anonymous"></script>
622+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" crossorigin="anonymous"></script>
623+
<script>
624+
(function () {
625+
var canvas = document.getElementById('epssChart');
626+
if (!canvas) return;
627+
628+
var raw = document.getElementById('epss-history-data');
629+
if (!raw) return;
630+
631+
var history = JSON.parse(raw.textContent || '[]');
632+
if (history.length === 0) return;
633+
634+
new Chart(canvas, {
635+
type: 'line',
636+
data: {
637+
labels: history.map(function (e) { return e.date; }),
638+
datasets: [{
639+
label: 'EPSS Score',
640+
data: history.map(function (e) { return e.score; }),
641+
borderColor: '#3273dc',
642+
backgroundColor: 'rgba(50, 115, 220, 0.08)',
643+
pointRadius: history.length > 60 ? 2 : 4,
644+
tension: 0.1,
645+
fill: true,
646+
}]
647+
},
648+
options: {
649+
responsive: true,
650+
scales: {
651+
y: {
652+
min: 0,
653+
max: 1,
654+
title: { display: true, text: 'EPSS Score (0–1)' }
655+
},
656+
x: {
657+
title: { display: true, text: 'Date' }
658+
}
659+
},
660+
plugins: {
661+
tooltip: {
662+
callbacks: {
663+
afterLabel: function (ctx) {
664+
var p = history[ctx.dataIndex].percentile;
665+
return p != null ? 'Percentile: ' + p : '';
666+
}
667+
}
668+
}
669+
}
670+
}
671+
});
672+
})();
673+
</script>
600674

601675
<script>
602676
function goToTab(tabName) {

vulnerabilities/views.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
9+
import json
910
import logging
1011

1112
from cvss.exceptions import CVSS2MalformedError
@@ -386,14 +387,29 @@ def get_context_data(self, **kwargs):
386387
):
387388
logging.error(f"CVSSMalformedError for {severity.scoring_elements}")
388389

389-
epss_severity = vulnerability.severities.filter(scoring_system="epss").first()
390+
epss_severities = vulnerability.severities.filter(
391+
scoring_system="epss"
392+
).order_by("published_at")
393+
390394
epss_data = None
391-
if epss_severity:
395+
epss_history_json = "[]"
396+
if epss_severities.exists():
397+
latest = epss_severities.last()
392398
epss_data = {
393-
"percentile": epss_severity.scoring_elements,
394-
"score": epss_severity.value,
395-
"published_at": epss_severity.published_at,
399+
"percentile": latest.scoring_elements,
400+
"score": latest.value,
401+
"published_at": latest.published_at,
396402
}
403+
epss_history_json = json.dumps(
404+
[
405+
{
406+
"date": e.published_at.strftime("%Y-%m-%d") if e.published_at else None,
407+
"score": float(e.value) if e.value else None,
408+
"percentile": float(e.scoring_elements) if e.scoring_elements else None,
409+
}
410+
for e in epss_severities
411+
]
412+
)
397413

398414
context.update(
399415
{
@@ -407,6 +423,7 @@ def get_context_data(self, **kwargs):
407423
"status": vulnerability.get_status_label,
408424
"history": vulnerability.history,
409425
"epss_data": epss_data,
426+
"epss_history_json": epss_history_json,
410427
}
411428
)
412429
return context

0 commit comments

Comments
 (0)