From 5b166c1577f1c8f507aabb041641d1a55a38d510 Mon Sep 17 00:00:00 2001 From: Rex P <106129829+another-rex@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:28:23 +1100 Subject: [PATCH] fix: HTML report tinkering (#1561) Made a series of changes to resolve the issues identified in #1528 Hosted an example here: https://another-rex.github.io/TestPages/Vulnerability%20Scan%20Report.html To make it easier to see the changes, when reviewing, use this link: https://github.com/google/osv-scanner/pull/1561/files/bd8d5211e77612b1a0a68a4b00db1d40535fe400..8223593ef84cda34da84a57aecd12d3258ea1463 which select the diffs **Excluding** the first commit (use shift to select multiple commits). That moves the files around which breaks all of git's diffing. No change other than moving the files and reindenting is done in that first commit. HTML: - Move to actual .js and .css file rather than .html files. - Alias and groupid tooltips now put each ID on a new line. - Can now click on the entire filter box to change it, not just on the text part. CSS: - Remove max-height in the inner tables, this was making it impossible to have tooltips that escape the container (at least I haven't figured out how to have both). - Tooltip box sizing is now dynamic with max-width - Tooltips now display upwards instead of downwards - Highlight source path better - Minor refactor to how the search box is laid out - Remove unused css lines. - Make iframe bg color black instead of white to avoid flash banging people. JS: - Remove all style edits in javascript, state changes are made with classes now. (TIL `classList.toggle()` function) - Basic linter pass (e.g. use const on variables, define all variables...etc) - Run showAllVulns() function at page load. --- internal/output/html.go | 21 +- .../output/html/base_image_template.gohtml | 2 +- internal/output/html/filter_template.gohtml | 32 +- .../output/html/package_table_template.gohtml | 28 +- .../output/html/package_view_template.gohtml | 2 +- internal/output/html/report_template.gohtml | 8 +- internal/output/html/script.html | 535 --------------- internal/output/html/script.js | 488 ++++++++++++++ internal/output/html/style.css | 628 ++++++++++++++++++ internal/output/html/style.html | 612 ----------------- .../html/vuln_table_entry_template.gohtml | 47 ++ .../output/html/vuln_table_template.gohtml | 75 +-- 12 files changed, 1223 insertions(+), 1255 deletions(-) delete mode 100644 internal/output/html/script.html create mode 100644 internal/output/html/script.js create mode 100644 internal/output/html/style.css delete mode 100644 internal/output/html/style.html create mode 100644 internal/output/html/vuln_table_entry_template.gohtml diff --git a/internal/output/html.go b/internal/output/html.go index e848a2cc626..09ffe725517 100644 --- a/internal/output/html.go +++ b/internal/output/html.go @@ -35,6 +35,18 @@ func formatRating(rating severity.Rating) string { return strings.ToLower(string(rating)) } +type VulnTableEntryArgument struct { + Element VulnResult + IsHidden bool +} + +func buildVulnTableEntryArgument(element VulnResult, isHidden bool) VulnTableEntryArgument { + return VulnTableEntryArgument{ + IsHidden: isHidden, + Element: element, + } +} + func PrintHTMLResults(vulnResult *models.VulnerabilityResults, outputWriter io.Writer) error { // htmlResult := BuildHTMLResults(vulnResult) result := BuildResults(vulnResult) @@ -48,10 +60,11 @@ func PrintHTMLResults(vulnResult *models.VulnerabilityResults, outputWriter io.W "add": func(a, b int) int { return a + b }, - "getFilteredVulnReasons": getFilteredVulnReasons, - "getBaseImageName": getBaseImageName, - "formatSlice": formatSlice, - "formatLayerCommand": formatLayerCommand, + "getFilteredVulnReasons": getFilteredVulnReasons, + "getBaseImageName": getBaseImageName, + "formatSlice": formatSlice, + "formatLayerCommand": formatLayerCommand, + "buildVulnTableEntryArgument": buildVulnTableEntryArgument, } tmpl := template.Must(template.New("").Funcs(funcMap).ParseFS(templates, TemplateDir)) diff --git a/internal/output/html/base_image_template.gohtml b/internal/output/html/base_image_template.gohtml index a3e2cf5c98a..60d92fa22bd 100644 --- a/internal/output/html/base_image_template.gohtml +++ b/internal/output/html/base_image_template.gohtml @@ -36,7 +36,7 @@ -
+
{{ range .AllLayers }} {{ $index := .Index }} {{ $originalCommand := .LayerMetadata.Command }} diff --git a/internal/output/html/filter_template.gohtml b/internal/output/html/filter_template.gohtml index 45568d4fc6c..f73b13c417c 100644 --- a/internal/output/html/filter_template.gohtml +++ b/internal/output/html/filter_template.gohtml @@ -9,7 +9,7 @@ keyboard_arrow_down
-
+

All layers ({{ .VulnCount.AnalysisCount.Regular }})

@@ -24,7 +24,7 @@ {{ $diffID := .LayerMetadata.DiffID }} {{ $longCommand := false }} {{ if gt (len $command) 109 }} - {{ $longCommand = true }} + {{ $longCommand = true }} {{ end }} {{ if gt .Count.AnalysisCount.Regular 0 }} @@ -34,7 +34,7 @@ {{ if $longCommand }} {{ $originalCommand }} {{ end }} -
+
{{ template "severity_summary_template.gohtml".Count.SeverityCount }}
{{ end }} @@ -54,28 +54,28 @@ keyboard_arrow_down -
+
{{ if .IsContainerScanning }} -
+
+ Default ({{ .VulnTypeSummary.All }}) + {{ end }} -
+
+ Project Vulnerabilities ({{ .VulnTypeSummary.Project }}) + {{ if .IsContainerScanning }} -
+
+ Operating system vulnerabilities ({{ .VulnTypeSummary.OS }}) + {{ end }} -
+
+ Uncalled/Unimportant ({{ .VulnTypeSummary.Hidden }}) +
diff --git a/internal/output/html/package_table_template.gohtml b/internal/output/html/package_table_template.gohtml index dc3dcc702be..80c05ab4919 100644 --- a/internal/output/html/package_table_template.gohtml +++ b/internal/output/html/package_table_template.gohtml @@ -30,19 +30,21 @@ {{ $element.Name }} {{ $element.InstalledVersion }} +
{{ if ne $element.VulnCount.FixableCount.UnFixed 0 }} - {{ if eq $element.VulnCount.FixableCount.Fixed 0}} -

No fix available

- {{ else }} -
-

Partial fixes available

- Upgrading the package will fix {{ $element.VulnCount.FixableCount.Fixed }} out of {{ - $element.VulnCount.AnalysisCount.Regular }} vulnerabilities -
- {{ end }} + {{ if eq $element.VulnCount.FixableCount.Fixed 0}} +

No fix available

+ All {{ $element.VulnCount.AnalysisCount.Regular }} vulnerabilities have no fixed version available. + {{ else }} +

Partial fixes available

+ Upgrading the package can fix {{ $element.VulnCount.FixableCount.Fixed }} out of {{ + $element.VulnCount.AnalysisCount.Regular }} vulnerabilities. + {{ end }} {{ else }} -

Fix available

+

Fix available

+ Upgrading the package can fix all {{ $element.VulnCount.FixableCount.Fixed }} vulnerabilities. {{ end }} +
{{ if ne $element.VulnCount.AnalysisCount.Regular 0 }} @@ -52,9 +54,9 @@ {{ end }} - + -
+
{{ if and $element.LayerDetail.LayerInfo.LayerMetadata (not (eq $element.LayerDetail.LayerInfo.LayerMetadata.Command "")) }} {{ $index := $element.LayerDetail.LayerIndex }} {{ $diffID := $element.LayerDetail.LayerInfo.LayerMetadata.DiffID }} @@ -82,7 +84,7 @@

Introduced in layer #{{ $index }}: 

{{ $command }}

- {{ $commandDetail }} + {{ $commandDetail }}
{{ else }}

Introduced in layer #{{ $index }}: 

diff --git a/internal/output/html/package_view_template.gohtml b/internal/output/html/package_view_template.gohtml index 9a4a23e3aee..1f83ab0a4bf 100644 --- a/internal/output/html/package_view_template.gohtml +++ b/internal/output/html/package_view_template.gohtml @@ -4,7 +4,7 @@
{{ range .Sources }}
-

Source: {{ .Name }}

+

Source: {{ .Name }}

{{ template "package_table_template.gohtml" .Packages }}
{{ end }} diff --git a/internal/output/html/report_template.gohtml b/internal/output/html/report_template.gohtml index 917faef7a84..19756b088cb 100644 --- a/internal/output/html/report_template.gohtml +++ b/internal/output/html/report_template.gohtml @@ -7,11 +7,12 @@ - {{ template "style.html" }} + - {{ template "script.html" }}
@@ -57,6 +58,9 @@
+ diff --git a/internal/output/html/script.html b/internal/output/html/script.html deleted file mode 100644 index e2bcdf5ede6..00000000000 --- a/internal/output/html/script.html +++ /dev/null @@ -1,535 +0,0 @@ - diff --git a/internal/output/html/script.js b/internal/output/html/script.js new file mode 100644 index 00000000000..3b14d23ca72 --- /dev/null +++ b/internal/output/html/script.js @@ -0,0 +1,488 @@ +const selectedTypeFilterValue = new Set(); +selectedTypeFilterValue.add("all"); +let selectedLayer = "all"; + +function quickFilterByLayer(DiffID, layerCommand) { + resetFilterText(); + applyFilters(selectedTypeFilterValue, DiffID); + const selectedDisplay = document.getElementById("layer-filter-selected"); + selectedDisplay.textContent = layerCommand; +} + +function showBaseImageLayer(imageID) { + const detailElementID = "base-image-details-" + imageID; + const detailsElement = document.getElementById(detailElementID); + + const icon = document.querySelector( + `#base-image-summary-${imageID} .material-icons` + ); // Select the icon within the row + + const hidBlock = detailsElement.classList.toggle("hide-block"); + icon.classList.toggle("expanded", !hidBlock); +} + +function showPackageDetails(detailsId) { + const detailsElement = document.getElementById( + "table-tr-" + detailsId + "-details" + ); + const icon = document.querySelector(`#table-tr-${detailsId} .material-icons`); // Select the icon within the row + + const hidBlock = detailsElement.classList.toggle("hide-block"); + icon.classList.toggle("expanded", !hidBlock); +} + +function openVulnInNewTab(inputString) { + const osvURL = `https://osv.dev/${inputString}`; + const tabs = document.getElementById("tabs"); + const tabSwitches = document.getElementById("tab-switch"); + + const existingTab = document.getElementById(inputString); + if (existingTab) { + openTab(inputString); + return; + } + + // Create the new tab div. + const newTab = document.createElement("div"); + newTab.id = inputString; // Set the ID to the input string + newTab.className = "tab osv-tab"; // Set the class name + + // Create the iframe element. + const iframe = document.createElement("iframe"); + iframe.src = osvURL; + + // Create a new tab button + const newTabButton = document.createElement("div"); + newTabButton.id = inputString + "-button"; + newTabButton.className = "tab-switch-button"; + newTabButton.onclick = function () { + openTab(inputString); + }; + + // Add

and elements to the button + const newTabTextContainer = document.createElement("div"); + newTabTextContainer.className = "tab-button-text-container"; + const newTabText = document.createElement("p"); + newTabText.textContent = inputString; + newTabTextContainer.appendChild(newTabText); + + const newTabButtonBorder = document.createElement("div"); + newTabButtonBorder.className = "tab-switch-button-border"; + newTabTextContainer.appendChild(newTabButtonBorder); + + newTabButton.appendChild(newTabTextContainer); + + const closeIcon = document.createElement("span"); + closeIcon.className = "material-icons"; + closeIcon.textContent = "close"; + // Add the onclick function to the close icon + closeIcon.onclick = function (event) { + event.stopPropagation(); // Prevent the click from opening the tab + closeVulnTab(inputString); + }; + + newTabButton.appendChild(closeIcon); + + // Add the iframe to the new tab div. + newTab.appendChild(iframe); + // Add the iframe to the container. + tabs.appendChild(newTab); + tabSwitches.appendChild(newTabButton); + + openTab(newTab.id); +} + +function closeVulnTab(inputString) { + const tabToRemove = document.getElementById(inputString); + const buttonToRemove = document.getElementById(inputString + "-button"); + const tabs = document.getElementById("tabs"); + const tabSwitches = document.getElementById("tab-switch"); + + if (tabToRemove && buttonToRemove) { + const nextTabButton = + buttonToRemove.nextElementSibling || + buttonToRemove.previousElementSibling; + + tabs.removeChild(tabToRemove); + tabSwitches.removeChild(buttonToRemove); + + if (nextTabButton) { + const nextTabId = nextTabButton.id.replace("-button", ""); + openTab(nextTabId); + } + } +} + +function openTab(activeTabId) { + const tabs = document.getElementsByClassName("tab"); + const tabButtons = document.getElementsByClassName("tab-switch-button"); + for (let i = 0; i < tabs.length; i += 1) { + tabs[i].classList.toggle("hide-block", tabs[i].id !== activeTabId); + tabButtons[i].classList.toggle( + "tab-switch-button-selected", + tabs[i].id === activeTabId + ); + } +} + +function hideAllFilterOptions() { + const containers = document.getElementsByClassName("filter-option-container"); + + for (const container of containers) { + container.classList.toggle("hide-block", true); + } +} + +function toggleFilter(input) { + const targetID = input + "-filter-option-container"; + let optionContainer = document.getElementById(targetID); + const containers = document.getElementsByClassName("filter-option-container"); + for (const loopContainer of containers) { + if (loopContainer.id === targetID) { + optionContainer.classList.toggle("hide-block"); + } else { + loopContainer.classList.toggle("hide-block", true); + } + } +} + +function showAndHideParentSections() { + const ecosystemContainers = document.querySelectorAll(".ecosystem-container"); + + ecosystemContainers.forEach(ecosystemContainer => { + const sourceContainers = + ecosystemContainer.querySelectorAll(".source-container"); + let ecosystemHasVisibleSources = false; + + sourceContainers.forEach(sourceContainer => { + const packageRows = sourceContainer.querySelectorAll(".package-tr"); + let sourceHasVisibleRows = false; + + packageRows.forEach(packageRow => { + let packageDetails = document.getElementById( + packageRow.id + "-details" + ); + const vulnRows = packageDetails.querySelectorAll(".vuln-tr"); + let packageHasVisibleRows = false; + vulnRows.forEach(vulnRow => { + if (!vulnRow.classList.contains("hide-block")) { + packageHasVisibleRows = true; + return; + } + }); + if (packageHasVisibleRows) { + sourceHasVisibleRows = true; + packageRow.classList.toggle("hide-block", false); + return; + } else { + packageRow.classList.toggle("hide-block", true); + packageDetails.classList.toggle("hide-block", true); + const icon = document.querySelector( + `#${packageRow.id} .material-icons` + ); + icon.classList.remove("expanded"); // Rotate back to 0 degrees + } + }); + + sourceContainer.classList.toggle("hide-block", !sourceHasVisibleRows); + + if (sourceHasVisibleRows) { + ecosystemHasVisibleSources = true; + return; + } + }); + + ecosystemContainer.classList.toggle( + "hide-block", + !ecosystemHasVisibleSources + ); + }); +} + +function showAllVulns() { + const vulnRows = document.getElementsByClassName("vuln-tr"); + for (const row of vulnRows) { + let isUncalled = row.classList.contains("uncalled-tr"); + row.classList.toggle("hide-block", isUncalled); + } + + showAndHideParentSections(); +} + +function applyFilters(selectedTypeFilterValue, selectedLayerFilterValue) { + // Show all vulnerabilities and then hide those that do not match the filter requirements. + showAllVulns(); + applyTypeFilter(selectedTypeFilterValue); + applyLayerFilter(selectedLayerFilterValue); + showAndHideParentSections(); +} + +function applyTypeFilter(selectedValue) { + updateTypeFilterText(selectedValue); + let selectedAll = selectedValue.has("all"); + let selectedProject = selectedValue.has("project"); + let selectedOS = selectedValue.has("os"); + let selectedUncalled = selectedValue.has("uncalled"); + if (selectedAll) { + selectedProject = true; + selectedOS = true; + } + + const ecosystemElements = document.querySelectorAll(".ecosystem-container"); + + ecosystemElements.forEach(ecosystemElement => { + const vulnElements = ecosystemElement.querySelectorAll(".vuln-tr"); + vulnElements.forEach(vuln => { + if (vuln.classList.contains("uncalled-tr")) { + vuln.classList.toggle("hide-block", !selectedUncalled); + } + if ( + (ecosystemElement.classList.contains("os-type") && !selectedOS) || + (ecosystemElement.classList.contains("project-type") && + !selectedProject) + ) { + vuln.classList.toggle("hide-block", true); + } + }); + }); +} + +function applyLayerFilter(selectedLayerID) { + const tableRows = document.querySelectorAll("tr.has-layer-info"); + tableRows.forEach(row => { + const rowLayerID = row.getAttribute("data-layer"); + if (selectedLayerID !== "all" && rowLayerID !== selectedLayerID) { + const packageDetails = document.getElementById(row.id + "-details"); + const vulnElements = packageDetails.querySelectorAll(".vuln-tr"); + vulnElements.forEach(vuln => { + vuln.classList.toggle("hide-block", true); + }); + } + }); +} + +function updateTypeFilterText(_selectedValue) { + const typeSelected = document.getElementById("type-filter-selected"); + const selectedVulnCount = document.getElementById("selected-count"); + + const allTypeCheckbox = document.getElementById("all-type-checkbox"); + const osTypeCheckbox = document.getElementById("os-type-checkbox"); + const projectTypeCheckbox = document.getElementById("project-type-checkbox"); + const uncalledTypeCheckbox = document.getElementById( + "uncalled-type-checkbox" + ); + + let selectedText = ""; + let selectedCount = 0; + + if (projectTypeCheckbox && projectTypeCheckbox.checked) { + selectedText += (selectedText ? ", " : "") + "Project"; + const projectTypeVulnCount = projectTypeCheckbox.getAttribute( + "data-type-project-count" + ); + selectedCount += parseInt(projectTypeVulnCount, 10); + } + if (osTypeCheckbox && osTypeCheckbox.checked) { + selectedText += (selectedText ? ", " : "") + "OS"; + const osTypeVulnCount = osTypeCheckbox.getAttribute("data-type-os-count"); + selectedCount += parseInt(osTypeVulnCount, 10); + } + if (uncalledTypeCheckbox && uncalledTypeCheckbox.checked) { + selectedText += (selectedText ? ", " : "") + "Unimportant"; + const uncalledTypeVulnCount = uncalledTypeCheckbox.getAttribute( + "data-type-uncalled-count" + ); + selectedCount += parseInt(uncalledTypeVulnCount, 10); + } + + if ( + allTypeCheckbox && + allTypeCheckbox.checked && + uncalledTypeCheckbox && + !uncalledTypeCheckbox.checked + ) { + selectedText = "Default"; + } + + typeSelected.textContent = selectedText; + selectedVulnCount.textContent = selectedCount; +} + +function resetFilterText() { + const layerSelected = document.getElementById("layer-filter-selected"); + const allLayerCheckedBox = document.getElementById("all-layer-checkbox"); + if (layerSelected) { + layerSelected.textContent = + "All layers (" + + allLayerCheckedBox.getAttribute("data-layer-all-count") + + ")"; + } + + const typeSelected = document.getElementById("type-filter-selected"); + const selectedVulnCount = document.getElementById("selected-count"); + const allTypeCheckedBox = document.getElementById("all-type-checkbox"); + const uncalledTypeCheckBox = document.getElementById( + "uncalled-type-checkbox" + ); + if (allTypeCheckedBox) { + typeSelected.textContent = "Default"; + selectedVulnCount.textContent = allTypeCheckedBox.getAttribute( + "data-type-all-count" + ); + allLayerCheckedBox.checked = true; + uncalledTypeCheckBox.checked = false; + } else { + const projectTypeCheckedBox = document.getElementById( + "project-type-checkbox" + ); + projectTypeCheckedBox.checked = true; + typeSelected.textContent = "Default"; + selectedVulnCount.textContent = projectTypeCheckedBox.getAttribute( + "data-type-project-count" + ); + uncalledTypeCheckBox.checked = false; + } +} + +function resetSearchText() { + const vulnSearchInput = document.getElementById("vuln-search"); + if (vulnSearchInput.value != "") { + vulnSearchInput.value = ""; + showAllVulns(); + } +} + +function resetTypeCheckbox() { + const allTypeCheckbox = document.getElementById("all-type-checkbox"); + const osTypeCheckbox = document.getElementById("os-type-checkbox"); + const projectTypeCheckbox = document.getElementById("project-type-checkbox"); + const uncalledTypeCheckbox = document.getElementById( + "uncalled-type-checkbox" + ); + + if (allTypeCheckbox) { + allTypeCheckbox.checked = true; + projectTypeCheckbox.checked = true; + if (osTypeCheckbox) { + osTypeCheckbox.checked = true; + } + uncalledTypeCheckbox.checked = false; + } +} + +document.addEventListener("DOMContentLoaded", function () { + resetFilterText(); + showAndHideParentSections(); + + // Implement filter for vulnerability types + const typeFilterOptions = document.getElementById( + "type-filter-option-container" + ); + + typeFilterOptions.addEventListener("change", function () { + resetSearchText(); + const changedElement = event.target; + const allTypesCheckbox = document.getElementById("all-type-checkbox"); + const projectCheckbox = document.getElementById("project-type-checkbox"); // Project vulnerabilities + const osCheckbox = document.getElementById("os-type-checkbox"); // OS vulnerabilities + const uncalledCheckbox = document.getElementById("uncalled-type-checkbox"); // OS vulnerabilities + selectedTypeFilterValue.clear(); + + if (allTypesCheckbox != null) { + if (changedElement == allTypesCheckbox) { + osCheckbox.checked = allTypesCheckbox.checked; + projectCheckbox.checked = allTypesCheckbox.checked; + if (allTypesCheckbox.checked === true) { + selectedTypeFilterValue.add("all"); + } + } + if (osCheckbox.checked === false || projectCheckbox.checked === false) { + allTypesCheckbox.checked = false; + } + + if (osCheckbox.checked) { + selectedTypeFilterValue.add("os"); + } + } + + if (projectCheckbox.checked) { + selectedTypeFilterValue.add("project"); + } + + if (uncalledCheckbox.checked) { + selectedTypeFilterValue.add("uncalled"); + } + + applyFilters(selectedTypeFilterValue, selectedLayer); + }); + + // Implement layer filter + const layerFilterOptionsContainer = document.getElementById( + "layer-filter-option-container" + ); + + if (layerFilterOptionsContainer) { + layerFilterOptionsContainer.addEventListener("click", event => { + const clickedOption = event.target.closest(".layer-filter-option"); + if (clickedOption) { + resetSearchText(); + selectedLayer = clickedOption.getAttribute("data-layer-hash"); + const selectedDisplay = document.getElementById( + "layer-filter-selected" + ); + const layerCommand = clickedOption.querySelector("p:first-child"); + selectedDisplay.textContent = layerCommand.textContent; + + hideAllFilterOptions(); + applyFilters(selectedTypeFilterValue, selectedLayer); + } + }); + } + + // Hide filter options when clicking other parts + const filterSections = document.querySelectorAll("div.filter"); + + document.addEventListener("click", event => { + if (![...filterSections].some(c => c.contains(event.target))) { + hideAllFilterOptions(); + } + }); + + // Search bar + const vulnSearchInput = document.getElementById("vuln-search"); + vulnSearchInput.addEventListener("keyup", event => { + resetFilterText(); + selectedTypeFilterValue.clear(); + selectedTypeFilterValue.add("all"); + selectedLayer = "all"; + resetTypeCheckbox(); + + const searchTerm = vulnSearchInput.value.trim().toLowerCase(); + + const vulnRows = document.querySelectorAll("[data-vuln-id]"); + + if (searchTerm === "") { + showAllVulns(); + return; + } + + vulnRows.forEach(row => { + const vulnID = row.getAttribute("data-vuln-id").toLowerCase(); + row.classList.toggle("hide-block", !vulnID.includes(searchTerm)); + }); + showAndHideParentSections(); + }); + + // Implement tooltips + document.querySelectorAll(".tooltip").forEach(elem => { + elem.addEventListener("mouseover", event => { + const rect = elem.getBoundingClientRect(); + const tooltipElem = elem.querySelector(".tooltiptext"); + + tooltipElem.style.left = rect.left + "px"; + tooltipElem.style.top = rect.top + "px"; + }); + }); + + showAllVulns(); +}); diff --git a/internal/output/html/style.css b/internal/output/html/style.css new file mode 100644 index 00000000000..14db2c16b47 --- /dev/null +++ b/internal/output/html/style.css @@ -0,0 +1,628 @@ +body { + margin: 0; + padding: 0; + background: #292929; + color: #fff; + overflow-y: scroll; + font-family: "Overpass Mono", monospace; + font-size: 12pt; + font-weight: 100; + min-width: fit-content; +} + +a { + color: inherit; +} + +.container { + max-width: 1400px; + margin: 50px auto; + padding: 20px; + border-radius: 4px; + box-shadow: 0 0 5px rgba(255, 255, 255, 0.15); + min-height: 90vh; +} + +h2 { + margin-top: 50px; + font-size: 15pt; +} + +h3 { + font-size: 12pt; + margin-top: 30px; +} + +h1, +h2, +h3 { + font-family: "Overpass Mono", monospace; + font-weight: normal; +} + +.ecosystem { + margin-top: 20px; +} + +header { + display: flex; + margin-bottom: 50px; + justify-content: space-between; +} + +#header-left { + display: flex; + align-items: center; +} + +.logo { + height: 20px; +} + +#header-left .vl { + border-left: 2px solid #fff; + height: 25px; + margin-left: 20px; + margin-right: 20px; +} + +#header-left h1 { + font-size: 23px; +} + +#header-right { + display: flex; + align-items: center; +} + +#header-right ::after { + display: inline-block; + content: " "; + background-image: url(https://osv.dev/static/img/external-link.svg); + width: 16px; + height: 16px; + margin-left: 3px; + vertical-align: middle; +} + +.material-icons { + vertical-align: middle; + transform: rotate(0deg); + transition: transform 0.2s ease; + user-select: none; +} + +.material-icons.expanded { + transform: rotate(90deg); +} + +.vuln-table { + width: 100%; + text-align: left; + margin-bottom: 40px; + margin-top: 10px; + color: rgba(255, 255, 255, 0.87); + border-spacing: 0; +} + +.vuln-table th { + border-bottom: 1px solid rgba(255, 255, 255, 0.33); + padding: 16px; +} + +.table-tr { + line-height: 30px; +} + +.table-tr td { + word-break: break-word; + width: 200px; + padding: 16px; +} + +.table-tr:hover { + background-color: rgba(255, 255, 255, 0.04); +} + +.table-tr-details > td { + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.vuln-tr { + display: table-row; +} + +.vuln-id { + color: #d9534f; +} + +.package-details { + text-align: left; + padding: 20px 50px 30px 50px; + display: block; + max-height: 800px; + overflow: auto; +} + +.package-details p { + margin-bottom: 10px; + margin-top: 10px; +} + +.table-tr:hover .open-in-tab-tag { + display: inline; +} + +.open-in-tab-tag { + display: none; + border: 1px solid rgba(255, 255, 255, 0.12); + padding: 5px; + border-radius: 4px; + width: fit-content; +} + +.open-in-tab-cell { + width: 90px !important; + cursor: pointer; +} + +.severity-cell { + width: 60px !important; + user-select: none; +} + +.fixable-tag { + border-radius: 4px; + max-width: 250px; + width: fit-content; + padding: 0 5px; + white-space: nowrap; + overflow: hidden; + text-align: center; + user-select: none; +} + +.has-fix { + white-space: break-spaces; + background-color: #6a6a6a; +} + +.no-fix { + background-color: #252525; + border: 1px solid #3c4043; +} + +.hide-block + .table-tr-details { + /* If details is after a hidden block, also hide details */ + + display: none; +} + +.uncalled-text { + color: #808080; +} + +#vuln-tab { + display: block; + margin-top: 30px; +} + +#tab-switch { + text-align: left; + border-bottom: 1px solid #6c6c6c; + padding-left: 20px; + display: flex; + margin-bottom: 50px; +} + +.tab-switch-button { + width: 180px; + outline: 0; + cursor: pointer; + background-color: transparent; + text-align: center; + display: flex; +} + +.tab-button-text-container { + flex-direction: column; + align-items: center; +} + +.tab-button-text-container p { + width: 160px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 2px solid transparent; + color: #5f6368; +} + +.tab-switch-button span { + cursor: pointer; + font-size: 16px; + display: none; +} + +.tab-switch-button-border { + width: 85%; + height: 3px; + background-color: transparent; + margin: auto; +} + +.tab-button-text-container:hover p { + color: #fff; +} + +.tab-switch-button-selected:hover span { + display: block; +} + +.tab-switch-button-selected .tab-switch-button-border { + background-color: #eee; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.tab-switch-button-selected p { + color: #fff; +} + +.osv-tab iframe { + width: 100%; + min-height: 1000px; + border: none; + background-color: #000; +} + +#vuln-tab { + display: block; +} + +.package-tr { + cursor: pointer; +} + +.icon-td { + width: 10px !important; +} + +#filter-section { + display: flex; + width: 60%; +} + +.filter-container { + margin-right: 20px; + max-width: 600px; +} + +.filter { + margin-top: 10px; + max-width: 600px; + min-width: 250px; + background-color: transparent; + border-radius: 8px; + overflow: hidden; + color: #fff; + border: 1px solid #ddd; + display: flex; + justify-content: space-between; + padding: 0 20px; +} + +#layer-filter { + width: 400px !important; +} + +.filter-selected { + white-space: nowrap; + overflow: hidden; +} + +.filter-option-container { + border: 1px solid #ddd; + border-radius: 8px; + position: absolute; + background-color: #292929; + min-width: 200px; + max-width: 1400px; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; + display: block; +} + +.filter-option { + border-bottom: 1px dotted #ddd; + padding: 15px 20px; + white-space: nowrap; + cursor: pointer; + display: block; +} + +.filter-option input, +.filter-option label { + cursor: pointer; +} + +#layer-filter-option-container { + width: 1400px; +} + +.layer-filter-option { + display: flex; + justify-content: space-between; +} + +.filter-option:hover { + background-color: rgba(255, 255, 255, 0.04); +} + +.filter-icon i { + position: relative; + top: 25%; +} + +#summary-section { + display: flex; + justify-content: space-between; + width: 100%; +} + +#severity-section { + overflow: hidden; +} + +.severity-long { + text-align: center; + min-width: 100px; + overflow: hidden; + white-space: nowrap; + user-select: none; +} + +.severity-long p { + font-size: 10pt; + padding: 5px 5px; + border-radius: 4px; + margin-right: 3px; +} + +.critical { + background-color: #ad0300; +} + +.high { + background-color: #ffa500; +} + +.medium { + background-color: #ffd700; + color: #292929; +} + +.low { + background-color: #53aa33; + color: #292929; +} + +.unknown { + background-color: #80868b; +} + +.severity-count-summary { + display: flex; +} + +.severity-short { + width: 40px; + user-select: none; +} + +.severity-short p { + text-align: center; +} + +.severity-short-first p { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.severity-short-last p { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.severity-cell .severity-short p { + border-radius: 4px; +} + +/* CSS for the search box */ +.search-box { + display: flex; + align-items: center; + width: 100%; + border: 1px solid #fff; + background-color: transparent; + border-radius: 8px; + margin-top: 30px; +} + +.search-box input[type="text"] { + flex: 1 0 auto; + padding: 15px 10px; + + border: none; + background-color: transparent; + color: #fff; + font-size: 12pt; +} + +.search-box input[type="text"]:focus { + outline: none; +} + +.search-icon { + height: fit-content; + padding: 13px; +} + +.package-detail-title { + color: #9aa0a6; +} + +.layer-command-container { + display: flex; +} + +.inner-table { + width: 100%; + border: 0.5px solid rgba(255, 255, 255, 0.12); + border-spacing: 0px; + border-radius: 4px; +} + +.inner-table th { + border-bottom: 0.5px solid rgba(255, 255, 255, 0.12); +} + +.inner-table td { + border-bottom: 0.5px solid rgba(255, 255, 255, 0.12); +} + +.expand-icon i.rotated { + transform: rotate(90deg); +} + +.tooltip { + position: relative; + display: inline-block; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 3px; +} + +.tooltip.no-underline { + text-decoration: none; +} + +.tooltip .tooltiptext { + visibility: hidden; + + background-color: black; + color: #fff; + + white-space: normal; + text-align: left; + line-height: 1.5; + + max-width: 300px; + width: max-content; + padding: 10px; + + border-radius: 4px; + /* Position the tooltip */ + position: fixed; + transform: translateY(-100%); + z-index: 999; +} + +.tooltip .tooltiptext.layer-tooltiptext { + white-space: normal; + margin-left: 0; + width: 1000px; + max-width: 1000px; + max-height: 200px; + overflow-x: hidden; + overflow-y: auto; +} + +.tooltip:hover .tooltiptext { + visibility: visible; +} + +.severity-count-summary .tooltip { + text-decoration: none; +} + +.flex-box { + display: flex; +} + +/* new added */ +.base-icon .tooltiptext { + margin-left: 0px; + background-color: #292929; +} + +div.title { + visibility: hidden; +} + +#base-image-section { + margin-top: 30px; + border: 1px solid #fff; + border-radius: 8px; + padding: 10px 20px; + max-height: 400px; + overflow-y: visible; + overflow-x: hidden; +} + +#base-image-table { + width: 100%; +} + +#base-image-table th { + text-align: left; + height: 30px; +} + +.layer-entry { + display: flex; + justify-content: space-between; + padding: 0 10px; + border-bottom: 1px dotted #666666; +} + +.base-image-summary { + cursor: pointer; +} + +.base-image-details { + display: block; + margin-bottom: 20px; +} + +.base-image-title { + width: 150px; + color: #9aa0a6; + height: 20px; +} + +.layer-command { + max-width: 1000px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.clickable { + cursor: pointer; +} + +.clickable-layer:hover { + background-color: rgba(255, 255, 255, 0.04); +} + +.source-path { + border: 1px #888 solid; + border-radius: 4px; + padding: 5px; + background-color: rgba(0, 0, 0, 0.2); +} + +.hide-block { + display: none !important; +} diff --git a/internal/output/html/style.html b/internal/output/html/style.html deleted file mode 100644 index 10e43224868..00000000000 --- a/internal/output/html/style.html +++ /dev/null @@ -1,612 +0,0 @@ - diff --git a/internal/output/html/vuln_table_entry_template.gohtml b/internal/output/html/vuln_table_entry_template.gohtml new file mode 100644 index 00000000000..ff47feda3ad --- /dev/null +++ b/internal/output/html/vuln_table_entry_template.gohtml @@ -0,0 +1,47 @@ +{{ $index := uniqueID }} +{{ $element := .Element }} + + + {{ if eq (len $element.GroupIDs) 1 }} +

{{ $element.ID }}
+ {{ else }} +
+
{{ $element.ID }}
+ Group IDs:
+ {{ range $rowIndex, $alias := $element.GroupIDs }} + {{ $alias }}
+ {{ end }} +
+
+ {{ end }} + + + {{ if eq (len $element.Aliases) 1 }} + {{ index $element.Aliases 0 }} + {{ else if gt (len $element.Aliases) 1}} +
+ {{ index $element.Aliases 0}}, ... + + {{ range $rowIndex, $alias := $element.Aliases }} + {{ $alias }}
+ {{ end }} +
+
+ {{ else }} + No Aliases + {{ end }} + + + +

+ {{$element.FixedVersion }}

+ + +
+

{{ $element.SeverityScore }}

+
+ + +

Open in tab

+ + diff --git a/internal/output/html/vuln_table_template.gohtml b/internal/output/html/vuln_table_template.gohtml index cb5b4179876..620e65cc12b 100644 --- a/internal/output/html/vuln_table_template.gohtml +++ b/internal/output/html/vuln_table_template.gohtml @@ -7,80 +7,13 @@ {{ range $rowIndex, $element := .RegularVulns }} - {{ $index := uniqueID }} - - - {{ if eq (len $element.GroupIDs) 1 }} -
{{ $element.ID }}
- {{ else }} -
-
{{ $element.ID }}
- Group IDs: {{ join $element.GroupIDs ", " }} -
- {{ end }} - - - {{ if eq (len $element.Aliases) 1 }} - {{ index $element.Aliases 0 }} - {{ else if gt (len $element.Aliases) 1}} -
- {{ index $element.Aliases 0}}, ... - {{ join $element.Aliases ", " }} -
- {{ end }} - - - -

- {{$element.FixedVersion }}

- - -
-

{{ $element.SeverityScore }}

-
- - -

Open in tab

- - + {{$args := buildVulnTableEntryArgument $element false}} + {{template "vuln_table_entry_template.gohtml" $args}} {{ end }} {{ range $rowIndex, $element := .HiddenVulns }} {{ $index := uniqueID }} - - - {{ if eq (len $element.GroupIDs) 1 }} -
{{ $element.ID }}
- {{ else }} -
-
{{ $element.ID }}
- Group IDs: {{ join $element.GroupIDs ", " }} -
- {{ end }} - - - {{ if eq (len $element.Aliases) 1 }} - {{ index $element.Aliases 0 }} - {{ else if gt (len $element.Aliases) 1}} -
- {{ index $element.Aliases 0}}... - {{ join $element.Aliases ", " }} -
- {{ end }} - - - -

{{$element.FixedVersion }}

- - -
-

{{ $element.SeverityScore }}

-
- - -

Open in tab

- - + {{$args := buildVulnTableEntryArgument $element true}} + {{template "vuln_table_entry_template.gohtml" $args}} {{ end }}