-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add dynamic vulnerability and image comparison pages (#943)
Signed-off-by: ltagliaferri <lisa.tagliaferri@gmail.com> Signed-off-by: Jamon Camisso <jamonation+git@gmail.com> Co-authored-by: ltagliaferri <lisa.tagliaferri@gmail.com>
- Loading branch information
1 parent
31ef3cd
commit 2638cfd
Showing
46 changed files
with
1,299 additions
and
189 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; | ||
|
||
// colour coding for sevrity on vulnerabilty info pages | ||
const severityColours = { | ||
Critical: "#F236F6", | ||
High: "#3443F4", | ||
Medium: "#1F2892", | ||
Low: "#16C0D7", | ||
Negligible: "#C5C5C5", | ||
Unknown: "#8C8C8C" | ||
} | ||
|
||
// used on image comparison and vulnerability info page search fields | ||
const searchFilter = document.querySelector("#filterInput"); | ||
const severityPicker = document.querySelector("#severity-picker"); | ||
if (searchFilter !== null) { | ||
searchFilter.value = null; | ||
searchFilter.addEventListener("keyup", () => { | ||
let filter = document.getElementById("filterInput").value.toLowerCase(); | ||
filterTable("rumble-images-external", filter); | ||
filterTable("rumble-images-chainguard", filter); | ||
if (severityPicker != null) { | ||
severityPicker.querySelector("label span").innerHTML = `<span>Severity<span class="bi-chevron-down" style="padding-left: 2rem;"></span></span>`; | ||
} | ||
}); | ||
} | ||
|
||
// taken from https://www.delftstack.com/howto/javascript/javascript-filter-table/ | ||
function filterTable(tableId, filter) { | ||
var table, tr, i, j; | ||
table = document.getElementById(tableId); | ||
tr = table.getElementsByTagName("tr"); | ||
for (i = 1; i < tr.length; i++) { | ||
tr[i].style.display = "none"; | ||
const tdArray = tr[i].getElementsByTagName("td"); | ||
for (var j = 0; j < tdArray.length; j++) { | ||
const cellValue = tdArray[j]; | ||
if (cellValue && cellValue.innerHTML.toLowerCase().indexOf(filter) > -1) { | ||
tr[i].style.display = ""; | ||
break; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// used on an image's vulnerability comparison page | ||
|
||
const image = document.location.pathname.replace(/\/$/, "").split("/").pop(); | ||
console.log(image); | ||
const data = await d3.csv(`https://storage.googleapis.com/chainguard-academy/cve-data/${image}.csv`); | ||
const displayColumns = ["Package", "Version", "Vulnerability", "Severity"]; | ||
const dataColumns = ["package", "version", "vulnerability", "s"]; | ||
const dataSorted = sortData(); | ||
|
||
makeTable("#rumble-images-external", dataSorted.theirs, dataSorted.theirVulns); | ||
makeTable("#rumble-images-chainguard", dataSorted.ours, dataSorted.ourVulns); | ||
|
||
function sortData() { | ||
let theirs = [], | ||
ours = []; | ||
|
||
data.forEach(function (row) { | ||
if (row.image.startsWith("cgr.dev")) { | ||
ours.push(row); | ||
} else { | ||
theirs.push(row); | ||
} | ||
}); | ||
|
||
let theirVulns = []; | ||
let ourVulns = []; | ||
|
||
theirs.forEach(function (row) { | ||
if (!(theirVulns.includes(row.vulnerability, 0))) { | ||
theirVulns.push(row.vulnerability) | ||
} | ||
}) | ||
|
||
ours.forEach(function (row) { | ||
if (!(ourVulns.includes(row.vulnerability, 0))) { | ||
ourVulns.push(row.vulnerability) | ||
} | ||
}) | ||
|
||
return { "theirs": theirs, "ours": ours, "theirVulns": theirVulns, "ourVulns": ourVulns } | ||
}; | ||
|
||
function makeTable(id, sortedData, vulnIDs) { | ||
var table = d3.select(id).append("table"), | ||
thead = table.append("thead"), | ||
tbody = table.append("tbody"); | ||
|
||
// append the header row | ||
thead.append("tr") | ||
.selectAll("th") | ||
.data(function () { | ||
return displayColumns.map(function (column) { | ||
if (column == "Vulnerability") { | ||
let count = vulnIDs.length; | ||
column = `ID [${count} unique]` | ||
} | ||
return column; | ||
}) | ||
}) | ||
.enter() | ||
.append("th") | ||
.text(function (column) { return column; }); | ||
|
||
|
||
|
||
// create a row for each object in the data | ||
var rows = tbody.selectAll("tr") | ||
.data(sortedData) | ||
.enter() | ||
.append("tr"); | ||
|
||
// create a cell in each row for each column | ||
rows.selectAll("td") | ||
.data(function (row) { | ||
return dataColumns.map(function (column) { | ||
let val = row[column]; | ||
if (column == "vulnerability") { | ||
let isProd = getEnvUrl(); | ||
if (isProd) { | ||
val = `<a href="/vulnerabilities/${val}">${val}</a>` | ||
} else { | ||
val = `<a href="/vulnerabilities/?id=${val}">${val}</a>` | ||
} | ||
} | ||
if (column == "s") { | ||
val = `<span style="color: ${severityColours[val]}; vertical-align: text-bottom; font-size: 8px; padding: 0.5rem; ">⬤</span><span style="vertical-align: text-bottom;">${val}</span>` | ||
} | ||
return { column: column, value: val }; | ||
}); | ||
}) | ||
.enter() | ||
.append("td") | ||
.html(function (d) { return d.value; }); | ||
|
||
if (vulnIDs.length == 0) { | ||
document.querySelector(id).insertAdjacentHTML("beforeend", `<p style="margin: 0; padding: 24px 8px;">No vulnerabilities detected</p>`); | ||
return; | ||
} | ||
}; | ||
|
||
// toggles between absolute and ?id= URLs for each page when rendering table links | ||
function getEnvUrl() { | ||
let host = document.location.host; | ||
if (host.match(`.+netlify.com.+`)) { | ||
return false | ||
} else if (host.match(`localhost:1313`)) { | ||
return false | ||
} else { | ||
return true | ||
} | ||
}; | ||
|
||
severityPicker.addEventListener("click", function (event) { | ||
if (event.target.tagName == "INPUT" || event.target.tagName == "LABEL") { | ||
return; | ||
} | ||
severityPicker.querySelector(".dropdown-content").visiblity = "hidden"; | ||
severityPicker.querySelector('input[type = "checkbox"]').checked = false; | ||
|
||
let filter = event.target.dataset.severity.toLowerCase(); | ||
filterTable("rumble-images-external", filter); | ||
filterTable("rumble-images-chainguard", filter); | ||
|
||
if (event.target.dataset.severity == "") { | ||
severityPicker.querySelector("label span").innerHTML = `<span>Severity<span class="bi-chevron-down" style="padding-left: 2rem;"></span></span>`; | ||
return; | ||
} | ||
|
||
severityPicker.querySelector("label span").innerHTML = `<span data-severity="${event.target.dataset.severity}"><span class="severity sev-${event.target.dataset.severity.toLowerCase()}" | ||
data-severity="${event.target.dataset.severity}">⬤</span>${event.target.dataset.severity}</span>`; | ||
searchFilter.value = null; | ||
}); | ||
severityPicker.addEventListener("mouseleave", function (event) { | ||
// severityPicker.querySelector(".dropdown-content").visiblity = "hidden"; | ||
severityPicker.querySelector('input[type = "checkbox"]').checked = false; | ||
}); | ||
|
||
const tds = document.querySelectorAll("#rumble .tables table tbody tr td"); | ||
tds.forEach(function (td) { | ||
let href = td.parentNode.childNodes[2].childNodes[0].href; | ||
|
||
td.addEventListener("click", function () { | ||
document.location = href; | ||
}); | ||
|
||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// fetches and renders information about individual vulnerabilities | ||
|
||
const displayColumns = ["Image", "First Detected", "Last Detected", "Days Vulnerable"]; | ||
const dataColumns = ["image", "first_seen", "last_seen", "duration"]; | ||
const id = getVulnID(); | ||
|
||
const data = await getData(`https://storage.googleapis.com/chainguard-academy/vulnerability-info/${id}.json`); | ||
|
||
showTitle(); | ||
showVuln(); | ||
|
||
function getVulnID() { | ||
let id = ""; | ||
let host = document.location.host; | ||
if (host.match(`.+netlify.app:443`) || host == "localhost:1313") { | ||
id = new URLSearchParams(document.location.search).get("id"); | ||
} else { | ||
id = document.location.pathname.split("/").pop(); | ||
} | ||
return id; | ||
} | ||
|
||
async function getData(url) { | ||
let res = await fetch(url); | ||
if (res.ok) { | ||
return await res.json(); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
function showTitle() { | ||
let vuln = data; | ||
const title = document.querySelector("#rumble-vuln h2#id"); | ||
title.innerHTML = `${id} <span style="color: ${severityColours[vuln.severity]}; vertical-align: text-bottom; font-size: 0.75rem; padding: 0.5rem; ">⬤</span><span style="vertical-align: text-bottom; font-size: 1rem;">${vuln.severity}</span>`; | ||
title.hidden = false; | ||
} | ||
|
||
function showVuln() { | ||
const div = document.querySelector("#vuln-details"); | ||
let vuln = data; | ||
|
||
let desc = document.createElement("p"); | ||
desc.innerHTML = `<p>${vuln.description}</p>` | ||
|
||
let sourceUrl = document.createElement("p"); | ||
sourceUrl.innerHTML = `<strong>Source</strong>: <a href="${vuln.url}">${vuln.url}</a>` | ||
|
||
div.insertAdjacentElement("beforeEnd", desc); | ||
div.insertAdjacentElement("beforeEnd", sourceUrl); | ||
} | ||
|
||
makeTable("#rumble-images-external", data.external_images); | ||
makeTable("#rumble-images-chainguard", data.chainguard_images); | ||
|
||
function makeTable(id, images) { | ||
|
||
// sort alphabetically by image name | ||
if (images !== null) { | ||
images.sort(function (a, b) { | ||
var keyA = a.image; | ||
var keyB = b.image; | ||
if (keyA < keyB) return -1; | ||
if (keyA > keyB) return 1; | ||
return 0; | ||
}); | ||
} else { | ||
document.querySelector(id).innerHTML = `<p style="margin: 0; padding: 24px 8px;">No vulnerabilities detected</p>`; | ||
return; | ||
} | ||
|
||
// assemble the table | ||
var table = d3.select(id).append("table") | ||
.attr("id", `${id}-1`), | ||
thead = table.append("thead"), | ||
tbody = table.append("tbody"); | ||
|
||
// append the header row | ||
thead.append("tr") | ||
.selectAll("th") | ||
.data(displayColumns) | ||
.enter() | ||
.append("th") | ||
.text(function (column) { return column; }); | ||
|
||
if (images === null) { | ||
return; | ||
} | ||
// create a row for each object in the data | ||
var rows = tbody.selectAll("tr") | ||
.data(images) | ||
.enter() | ||
.append("tr"); | ||
|
||
// create a cell in each row for each column | ||
rows.selectAll("td") | ||
.data(function (row) { | ||
return dataColumns.map(function (column) { | ||
let val = row[column]; | ||
if (column == "first_seen") { | ||
val = row.dates[0]; | ||
val = val.split("T", 1)[0]; | ||
} | ||
if (column == "last_seen") { | ||
val = row.dates[row.dates.length - 1]; | ||
val = val.split("T", 1)[0] // get rid of timestamps | ||
} | ||
if (column == "duration") { | ||
val = row.dates.length; | ||
} | ||
return { column: column, value: val }; | ||
}); | ||
}) | ||
.enter() | ||
.append("td") | ||
.html(function (d) { return d.value; }); | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.