Skip to content

Commit c0ca85a

Browse files
poojaTHAMBpoojathamb
authored andcommitted
Make CWE insights chart interactive
Signed-off-by: Pooja Thamb <poojathamb@gmail.com>
1 parent 4784e2a commit c0ca85a

4 files changed

Lines changed: 184 additions & 15 deletions

File tree

‎insights/static/insights/js/renderers.js‎

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,65 @@ export const renderers = {
6060
bb.generate(bbConfig);
6161
},
6262

63-
colored_bar(id, config) {
64-
const monoColor = config.color || getCssVar("--bulma-link");
65-
bb.generate({
66-
bindto: `#chart-${id}`,
67-
data: { x: "x", columns: config.columns, type: "bar", color: () => monoColor },
68-
axis: {
69-
rotated: true,
70-
x: { type: "category", label: { text: config.x_label || "CWE", position: "outer-middle" } },
71-
y: { label: { text: config.y_label || "Advisories", position: "outer-center" }, tick: { format: formatWholeNumbersOnly } }
72-
},
73-
tooltip: { format: { title: x => config.full_labels?.[x] || config.columns[0][x + 1], value: val => val.toLocaleString() } },
74-
legend: { show: false }
63+
colored_bar(id, config) {
64+
const chartContainer = document.getElementById(`chart-${id}`);
65+
if (!chartContainer) return;
66+
67+
const labels = config.columns[0].slice(1);
68+
const values = config.columns[1].slice(1);
69+
const monoColor = config.color || getCssVar("--bulma-link");
70+
71+
chartContainer.innerHTML = "";
72+
73+
labels.forEach((label, index) => {
74+
const row = document.createElement("div");
75+
row.style.marginBottom = "12px";
76+
row.style.cursor = "pointer";
77+
78+
const title = document.createElement("div");
79+
title.textContent =
80+
config.full_labels?.[index] || label;
81+
82+
const bar = document.createElement("div");
83+
bar.style.height = "28px";
84+
bar.style.width = `${Math.max(values[index] * 10, 20)}px`;
85+
bar.style.maxWidth = "100%";
86+
bar.style.backgroundColor = monoColor;
87+
bar.style.borderRadius = "4px";
88+
bar.style.marginTop = "4px";
89+
bar.textContent = values[index].toLocaleString();
90+
bar.style.color = "white";
91+
bar.style.paddingLeft = "8px";
92+
bar.style.lineHeight = "28px";
93+
94+
row.appendChild(title);
95+
row.appendChild(bar);
96+
97+
row.addEventListener("click", async () => {
98+
const cweId = label.replace("CWE-", "");
99+
100+
try {
101+
const response = await fetch(
102+
`/insights/cwe/${cweId}/advisories/`
103+
);
104+
105+
if (!response.ok) {
106+
throw new Error("Failed to fetch advisories");
107+
}
108+
109+
const data = await response.json();
110+
111+
showCweAdvisories(label, data.advisories);
112+
} catch (error) {
113+
console.error("Error loading CWE advisories:", error);
114+
showCweAdvisories(label, []);
115+
}
75116
});
76-
},
117+
118+
chartContainer.appendChild(row);
119+
});
120+
},
121+
77122

78123
scatter(id, config) {
79124
const [, ...buckets] = config.columns[0];
@@ -146,3 +191,76 @@ export const renderers = {
146191
});
147192
}
148193
};
194+
195+
function showCweAdvisories(cwe, advisories) {
196+
let modal = document.getElementById("cwe-advisories-modal");
197+
198+
if (!modal) {
199+
modal = document.createElement("div");
200+
modal.id = "cwe-advisories-modal";
201+
modal.className = "modal";
202+
203+
modal.innerHTML = `
204+
<div class="modal-background"></div>
205+
<div class="modal-card">
206+
<header class="modal-card-head">
207+
<p class="modal-card-title" id="cwe-advisories-title"></p>
208+
<button class="delete" aria-label="close"></button>
209+
</header>
210+
211+
<section class="modal-card-body" id="cwe-advisories-body">
212+
</section>
213+
</div>
214+
`;
215+
216+
document.body.appendChild(modal);
217+
218+
modal.querySelector(".modal-background").addEventListener(
219+
"click",
220+
() => modal.classList.remove("is-active")
221+
);
222+
223+
modal.querySelector(".delete").addEventListener(
224+
"click",
225+
() => modal.classList.remove("is-active")
226+
);
227+
}
228+
229+
document.getElementById("cwe-advisories-title").textContent =
230+
`${cwe} Advisories`;
231+
232+
const body = document.getElementById("cwe-advisories-body");
233+
234+
if (!advisories.length) {
235+
body.innerHTML = `
236+
<p class="has-text-grey">
237+
No advisories found for ${cwe}.
238+
</p>
239+
`;
240+
} else {
241+
body.innerHTML = `
242+
<div class="content">
243+
<p>
244+
<strong>${advisories.length}</strong>
245+
advisories associated with ${cwe}
246+
</p>
247+
<ul>
248+
${advisories.map(advisory => `
249+
<li>
250+
<a href="${advisory.url}" target="_blank">
251+
${advisory.avid}
252+
</a>
253+
${
254+
advisory.summary
255+
? `<p class="has-text-grey">${advisory.summary}</p>`
256+
: ""
257+
}
258+
</li>
259+
`).join("")}
260+
</ul>
261+
</div>
262+
`;
263+
}
264+
265+
modal.classList.add("is-active");
266+
}

‎insights/tests/test_views.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.urls import reverse
33

44
from insights.models import DailySnapshot
5-
from vulnerabilities.models import PackageV2
5+
from vulnerabilities.models import AdvisoryV2, AdvisoryWeakness, PackageV2
66

77

88
class TestInsightsViews(TestCase):
@@ -43,3 +43,27 @@ def test_dashboard_search_no_results(self):
4343
response = self.client.get("/insights/severity_panel/?q=pkg:pypi/does_not_exist")
4444
self.assertEqual(response.status_code, 200)
4545
self.assertEqual(response.context["search_error"], "No data available for this query.")
46+
47+
def test_cwe_advisories(self):
48+
weakness = AdvisoryWeakness.objects.create(cwe_id=200)
49+
50+
advisory = AdvisoryV2.objects.create(
51+
datasource_id="test-datasource",
52+
pipeline_id="test-pipeline",
53+
advisory_id="TEST-200",
54+
avid="test-advisory-200",
55+
unique_content_id="test-content-200",
56+
url="https://example.com/test-advisory-200",
57+
summary="Test advisory for CWE-200",
58+
is_latest=True,
59+
)
60+
advisory.weaknesses.add(weakness)
61+
62+
response = self.client.get("/insights/cwe/200/advisories/")
63+
64+
self.assertEqual(response.status_code, 200)
65+
self.assertEqual(len(response.json()["advisories"]), 1)
66+
self.assertEqual(
67+
response.json()["advisories"][0]["avid"],
68+
"test-advisory-200",
69+
)

‎insights/urls.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@
1313

1414
urlpatterns = [
1515
path("", views.insights_dashboard, name="insights-dashboard"),
16+
path(
17+
"cwe/<int:cwe_id>/advisories/",
18+
views.cwe_advisories,
19+
name="cwe_advisories",
20+
),
1621
path("<str:panel_id>/", views.insights_dashboard, name="insights-panel"),
17-
]
22+
]

‎insights/views.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99
from django.db.models import Count
10+
from django.http import JsonResponse
1011
from django.shortcuts import redirect
1112
from django.shortcuts import render
1213

@@ -60,6 +61,27 @@ def _cwe_search(search_query):
6061

6162
return {"pkg-cwe-bar": chart_data}, error
6263

64+
def cwe_advisories(request, cwe_id):
65+
"""Return the latest advisories associated with a CWE."""
66+
advisories = (
67+
AdvisoryV2.objects.filter(
68+
weaknesses__cwe_id=cwe_id,
69+
is_latest=True,
70+
)
71+
.distinct()
72+
)
73+
74+
data = [
75+
{
76+
"avid": advisory.avid,
77+
"url": advisory.get_absolute_url(),
78+
"summary": advisory.summary,
79+
}
80+
for advisory in advisories
81+
]
82+
83+
return JsonResponse({"advisories": data})
84+
6385

6486
def insights_dashboard(request, panel_id=None):
6587
"""Dashboard view to display insights."""

0 commit comments

Comments
 (0)