Skip to content

Commit ff85262

Browse files
committed
Add EPSS insights(trend line and history table)
Signed-off-by: Sampurna Pyne <sampurnapyne1710@gmail.com>
1 parent 4702c06 commit ff85262

4 files changed

Lines changed: 175 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Pipfile
9393

9494
# editors
9595
.vscode
96+
*.code-workspace
9697
# PyCharm
9798
.idea/
9899

vulnerabilities/templates/advisory_detail.html

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,61 @@
518518
{% endif %}
519519
</tbody>
520520
</table>
521+
522+
{% if epss_history_data|length > 1 %}
523+
<div class="has-text-weight-bold tab-nested-div ml-1 mb-3 mt-5">
524+
EPSS Score History
525+
</div>
526+
527+
<div class="mb-4 ml-1">
528+
<button id="btn-load-epss-chart" class="button is-small is-info is-outlined mr-2">
529+
See Trend
530+
</button>
531+
<button id="btn-load-epss-table" class="button is-small is-info is-outlined">
532+
See Table
533+
</button>
534+
</div>
535+
536+
<style>
537+
.bb-tooltip-container {
538+
min-width: 220px !important;
539+
}
540+
.bb-tooltip th, .bb-tooltip td {
541+
white-space: nowrap !important;
542+
}
543+
</style>
544+
545+
<div id="epss-chart-wrap" style="display:none;">
546+
<div class="has-text-weight-bold tab-nested-div ml-1 mb-3">
547+
30-Day Trend
548+
</div>
549+
<div id="epss-chart" style="width:100%; height:260px;"></div>
550+
</div>
551+
552+
<div id="epss-history-table-wrap" style="display:none; margin-top:2rem;">
553+
<div class="has-text-weight-bold tab-nested-div ml-1 mb-3">
554+
EPSS History Table
555+
</div>
556+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth gray-header-border" id="epss-history-table">
557+
<thead>
558+
<tr>
559+
<th>Published At</th>
560+
<th>EPSS Score</th>
561+
<th>Percentile</th>
562+
</tr>
563+
</thead>
564+
<tbody>
565+
{% for row in epss_history_data reversed %}
566+
<tr>
567+
<td>{{ row.published_at|date:"M d, Y" }}</td>
568+
<td>{{ row.score|default_if_none:"—" }}</td>
569+
<td>{{ row.percentile|default_if_none:"—" }}</td>
570+
</tr>
571+
{% endfor %}
572+
</tbody>
573+
</table>
574+
</div>
575+
{% endif %}
521576
{% else %}
522577
<p>No EPSS data available for this advisory.</p>
523578
{% endif %}
@@ -736,6 +791,11 @@
736791

737792
<script src="{% static 'js/main.js' %}" crossorigin="anonymous"></script>
738793

794+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/billboard.js@3/dist/billboard.min.css">
795+
<script src="https://cdn.jsdelivr.net/npm/billboard.js@3/dist/billboard.pkgd.min.js"></script>
796+
797+
{{ epss_history_data|json_script:"epss-history-data" }}
798+
739799
<script>
740800
function goToTab(tabName) {
741801
const activeLink = document.querySelector('div.tabs.is-boxed li.is-active');
@@ -752,4 +812,6 @@
752812
}
753813
</script>
754814

815+
<script src="{% static 'js/advisory_detail.js' %}"></script>
816+
755817
{% endblock %}

vulnerabilities/views.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,23 @@
2222
from django.core.exceptions import ValidationError
2323
from django.core.mail import send_mail
2424
from django.db.models import Exists
25+
from django.db.models import FloatField
26+
from django.db.models import Max
2527
from django.db.models import OuterRef
2628
from django.db.models import Prefetch
2729
from django.db.models import Q
30+
from django.db.models.functions import Cast
31+
from django.db.models.functions import TruncDate
32+
from django.http import HttpRequest
2833
from django.http import HttpResponse
34+
from django.http import JsonResponse
2935
from django.http.response import Http404
3036
from django.shortcuts import get_object_or_404
3137
from django.shortcuts import render
3238
from django.urls import reverse_lazy
3339
from django.views import View
3440
from django.views import generic
41+
from django.views.decorators.http import require_safe
3542
from django.views.generic.detail import DetailView
3643
from django.views.generic.edit import FormMixin
3744
from django.views.generic.edit import FormView
@@ -739,6 +746,44 @@ def add_ssvc(ssvc):
739746
add_ssvc(ssvc)
740747

741748
context["ssvcs"] = ssvc_entries
749+
750+
# EPSS history
751+
cves = {
752+
alias_obj.alias
753+
for alias_obj in advisory.aliases.all()
754+
if alias_obj.alias.startswith("CVE-")
755+
}
756+
if advisory.advisory_id and advisory.advisory_id.startswith("CVE-"):
757+
cves.add(advisory.advisory_id)
758+
759+
if cves:
760+
epss_scores_queryset = (
761+
models.AdvisorySeverity.objects.filter(
762+
advisories__advisory_id__in=cves,
763+
scoring_system=EPSS.identifier,
764+
published_at__isnull=False,
765+
)
766+
.annotate(pub_date=TruncDate("published_at"))
767+
.values("pub_date")
768+
.annotate(
769+
max_score=Max(Cast("value", FloatField())),
770+
max_percentile=Max(Cast("scoring_elements", FloatField())),
771+
)
772+
.order_by("-pub_date")[:30]
773+
)
774+
775+
epss_history_data = [
776+
{
777+
"score": record["max_score"],
778+
"percentile": record["max_percentile"],
779+
"published_at": record["pub_date"],
780+
}
781+
for record in epss_scores_queryset
782+
]
783+
epss_history_data.reverse()
784+
else:
785+
epss_history_data = []
786+
742787
context.update(
743788
{
744789
"advisory": advisory,
@@ -750,6 +795,7 @@ def add_ssvc(ssvc):
750795
"weaknesses": weaknesses_present_in_db,
751796
"status": advisory.get_status_label,
752797
"epss_data": epss_data,
798+
"epss_history_data": epss_history_data,
753799
}
754800
)
755801
return context
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
(function () {
2+
let epssChartInstance = null;
3+
4+
const [btnChart, btnTable, chartWrap, tableWrap] =
5+
["btn-load-epss-chart", "btn-load-epss-table", "epss-chart-wrap", "epss-history-table-wrap"]
6+
.map(id => document.getElementById(id));
7+
8+
const getHistoryData = () => JSON.parse(document.getElementById('epss-history-data')?.textContent || "[]");
9+
const toggleDisplay = (el) => el.style.display = el.style.display === "none" ? "block" : "none";
10+
11+
function renderChart() {
12+
const data = getHistoryData();
13+
if (!data.length) return;
14+
15+
toggleDisplay(chartWrap);
16+
if (chartWrap.style.display === "none" || epssChartInstance) return;
17+
18+
const history = [];
19+
const map = new Map(data.map(h => [new Date(h.published_at + "T00:00:00").setHours(0,0,0,0), h]));
20+
21+
const end = new Date(data[data.length - 1].published_at + "T00:00:00").setHours(0,0,0,0);
22+
let start = new Date(end);
23+
start.setDate(start.getDate() - 30);
24+
25+
const actualStart = new Date(data[0].published_at + "T00:00:00").setHours(0,0,0,0);
26+
start = new Date(Math.max(start.getTime(), actualStart));
27+
28+
for (let d = start; d.getTime() <= end; d.setDate(d.getDate() + 1)) {
29+
history.push(map.get(d.getTime()) || {
30+
published_at: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`,
31+
score: null, percentile: null
32+
});
33+
}
34+
try {
35+
epssChartInstance = bb.generate({
36+
bindto: "#epss-chart",
37+
size: { height: 260 }, padding: { right: 25 },
38+
data: {
39+
x: "Date", xFormat: "%Y-%m-%d",
40+
columns: [
41+
["Date", ...history.map(h => h.published_at || "")],
42+
["Score", ...history.map(h => h.score === null ? null : parseFloat(h.score))],
43+
["Percentile", ...history.map(h => h.percentile === null ? null : parseFloat(h.percentile))],
44+
],
45+
type: "line", colors: { Score: "#df25e6ff", Percentile: "#00d1b2" },
46+
},
47+
//Handles missing dates
48+
line: { connectNull: false },
49+
axis: {
50+
x: { type: "timeseries", tick: { format: "%b %d", count: Math.min(history.length, 6), fit: true } },
51+
y: { min: 0, max: 1, padding: { top: 10, bottom: 0 }, tick: { format: v => v.toFixed(4) } },
52+
},
53+
point: { r: 3 }, legend: { show: true },
54+
tooltip: {
55+
format: {
56+
title: d => d instanceof Date ? `${d.toLocaleString('en-us', {month: 'short'})} ${String(d.getDate()).padStart(2, '0')}, ${d.getFullYear()}` : String(d),
57+
value: v => v.toFixed(5),
58+
},
59+
},
60+
});
61+
} catch (e) { console.error("[epss-chart]", e); }
62+
}
63+
64+
btnChart?.addEventListener("click", renderChart);
65+
btnTable?.addEventListener("click", () => toggleDisplay(tableWrap));
66+
})();

0 commit comments

Comments
 (0)