diff --git a/render/render.go b/render/render.go
index d86c3b0..c17193c 100644
--- a/render/render.go
+++ b/render/render.go
@@ -1,9 +1,12 @@
package render
import (
- _ "embed"
+ "embed"
"encoding/json"
+ "io/fs"
"os"
+ "path/filepath"
+ "slices"
"text/template"
k8s "github.com/aquasecurity/trivy/pkg/k8s/report"
@@ -11,8 +14,10 @@ import (
"golang.org/x/xerrors"
)
-//go:embed template/html.tpl
-var htmlTmpl []byte
+//go:embed templates/*
+var templates embed.FS
+
+var extensions = []string{".tpl", ".js", ".css"}
func Render(fileName string, inputData []byte) error {
var kubernetes k8s.Report
@@ -31,7 +36,23 @@ func Render(fileName string, inputData []byte) error {
results = append(results, resource.Results...)
}
- tmpl, err := template.New("temp").Parse(string(htmlTmpl))
+ templateFS, err := fs.Sub(templates, "templates")
+ if err != nil {
+ return xerrors.Errorf("error loading templates: %w", err)
+ }
+
+ files, err := collectFiles(templateFS)
+ if err != nil {
+ return xerrors.Errorf("error collecting files: %w", err)
+ }
+
+ tmpl, err := template.New("temp").Funcs(template.FuncMap{
+ "toJSON": func(v interface{}) (string, error) {
+ bytes, err := json.Marshal(v)
+ return string(bytes), err
+ },
+ }).ParseFS(templateFS, files...)
+
if err != nil {
return xerrors.Errorf("error parsing template: %v\n", err)
}
@@ -42,9 +63,34 @@ func Render(fileName string, inputData []byte) error {
}
defer output.Close()
- if err = tmpl.Execute(output, results); err != nil {
+ if err = tmpl.ExecuteTemplate(output, "html.tpl", results); err != nil {
return xerrors.Errorf("error executing template: %v\n", err)
}
return nil
}
+
+func collectFiles(templateFS fs.FS) ([]string, error) {
+ var files []string
+ err := fs.WalkDir(templateFS, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return xerrors.Errorf("error listing files in %s: %w", path, err)
+ }
+
+ if d.IsDir() {
+ return nil
+ }
+
+ if slices.Contains(extensions, filepath.Ext(path)) {
+ files = append(files, path)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, xerrors.Errorf("error listing files: %w", err)
+ }
+
+ return files, nil
+}
diff --git a/render/render_test.go b/render/render_test.go
index a04308d..30c74f0 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -22,6 +22,11 @@ func TestRender(t *testing.T) {
jsonPath: "testdata/input/happy.json",
goldenPath: "testdata/golden/happy.html",
},
+ {
+ name: "graph",
+ jsonPath: "testdata/input/graph.json",
+ goldenPath: "testdata/golden/graph.html",
+ },
{
name: "happy k8s",
jsonPath: "testdata/input/happy-k8s.json",
diff --git a/render/template/html.tpl b/render/template/html.tpl
deleted file mode 100644
index 36f3b36..0000000
--- a/render/template/html.tpl
+++ /dev/null
@@ -1,739 +0,0 @@
-
-
-
-
-
- Trivy Report
-
-
-
-
-
-
-
-
-
-{{- if . }}
- Trivy Report - {{ ( index . 0 ).Target }}
-
-
-
-
-
-
-{{- range . }}
-{{- if or .Vulnerabilities .Misconfigurations .Secrets}}
-
-
-{{- if .Vulnerabilities }}
-
-
-
-
-
- {{- range .Vulnerabilities }}
-
- | {{ .PkgName }} |
- {{ .VulnerabilityID }} |
- {{ .Vulnerability.Severity }} |
- {{ .InstalledVersion }} |
- {{ .FixedVersion }} |
-
- {{- range .Vulnerability.References }}
- {{ . }}
- {{- end }}
- |
-
- {{- end}}
-
-
-{{- end }}
-
- {{- if .Misconfigurations }}
-
-
-
-
-
-
- {{- range .Misconfigurations }}
-
- | {{ .Type}} |
- {{ .ID }} |
- {{ .Title }} |
- {{ .Severity }} |
- {{ .Message }}
-
- {{ .PrimaryURL }}
-
- |
-
- {{- end}}
-
-
-{{- end }}
-
-{{- if .Secrets }}
-
-
- {{ .Target }}
- ({{ .Class }})
-
-
-
- {{- $target := .Target }}
- {{- range .Secrets }}
-
-
- {{ .Severity }}
- {{.Category}} ({{ .RuleID }})
-
-
{{ .Title }}
-
- {{ $target}}
- :
-
- {{ if eq .StartLine .EndLine }}
- {{ .StartLine }}
- {{ else }}
- {{ .StartLine }} - {{ .EndLine }}
- {{ end }}
-
-
- {{- range .Code.Lines }}
- {{- if .IsCause}}
-
- {{- else}}
-
- {{- end }}
- {{- end}}
-
-
-
- {{- end}}
-
- {{- end}}
-
-{{- if .Packages }}
-
-
-
-
-
-
-
- {{- range .Packages }}
-
- | {{ .ID }} |
- {{ .Name }} |
- {{ .Version }} |
- {{ .SrcName }} |
- {{ .SrcVersion }} |
-
- {{- end}}
-
-
-{{- end }}
-
-{{- end }}
-{{- end }}
-{{- else }}
- Trivy Returned Empty Report
-{{- end }}
-
-
-
\ No newline at end of file
diff --git a/render/templates/html.tpl b/render/templates/html.tpl
new file mode 100644
index 0000000..0a29b84
--- /dev/null
+++ b/render/templates/html.tpl
@@ -0,0 +1,327 @@
+
+
+
+
+
+ Trivy Report
+
+
+
+
+{{- if . }}
+Trivy Report - {{ ( index . 0 ).Target }}
+
+
+
+
+
+
+
+
+
+{{- range $resultIndex, $result := . }}
+{{- if or .Vulnerabilities .Misconfigurations .Secrets}}
+
+
+{{- if .Vulnerabilities }}
+
+
+
+
+
+ {{- range .Vulnerabilities }}
+
+ | {{ .PkgName }} |
+ {{ .VulnerabilityID }} |
+ {{ .Vulnerability.Severity }} |
+ {{ .InstalledVersion }} |
+ {{ .FixedVersion }} |
+
+ {{- range .Vulnerability.References }}
+ {{ . }}
+ {{- end }}
+ |
+
+ {{- end}}
+
+
+{{- end }}
+
+
+{{- if .Misconfigurations }}
+
+
+
+
+
+
+
+ {{- range $index, $misc:= .Misconfigurations }}
+
+ | {{ .Type}} |
+ {{ .ID }} |
+ {{ .Title }} |
+ {{ .Severity }} |
+ {{ .Message }}
+
+ {{ .PrimaryURL }}
+
+ |
+
+ {{- end}}
+
+
+
+
+
+{{- end }}
+
+
+{{- if .Secrets }}
+
+
+ {{ .Target }}
+ ({{ .Class }})
+
+
+
+ {{- $target := .Target }}
+ {{- range .Secrets }}
+
+
+ {{ .Severity }}
+ {{.Category}} ({{ .RuleID }})
+
+
{{ .Title }}
+
+ {{ $target}}
+ :
+
+ {{ if eq .StartLine .EndLine }}
+ {{ .StartLine }}
+ {{ else }}
+ {{ .StartLine }} - {{ .EndLine }}
+ {{ end }}
+
+
+ {{- range .Code.Lines }}
+ {{- if .IsCause}}
+
+ {{- else}}
+
+ {{- end }}
+ {{- end}}
+
+
+
+ {{- end}}
+
+{{- end}}
+
+{{- if .Packages }}
+
+
+
+
+
+
+
+ {{- range .Packages }}
+
+ | {{ .ID }} |
+ {{ .Name }} |
+ {{ .Version }} |
+ {{ .SrcName }} |
+ {{ .SrcVersion }} |
+
+ {{- end}}
+
+
+{{- end }}
+
+{{- end }}
+
+{{- end }}
+{{- else }}
+ Trivy Returned Empty Report
+{{- end }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/render/templates/src/components/graph.js b/render/templates/src/components/graph.js
new file mode 100644
index 0000000..bedca22
--- /dev/null
+++ b/render/templates/src/components/graph.js
@@ -0,0 +1,353 @@
+const codeNode = 1 << 1;
+const occurrenceNode = 1 << 2;
+let inWindow = false;
+let currentTable;
+let cy = cytoscape({
+ container: document.querySelector('.graph'),
+ elements: [],
+ style: [
+ {
+ selector: 'node',
+ style: {
+ label: 'data(label)',
+ 'background-color': '#666',
+ color: '#000',
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ width: '80px',
+ height: '80px',
+ },
+ },
+ {
+ selector: `node[type=${codeNode}]`,
+ style: {
+ 'background-color': '#c8c8c8',
+ 'font-size': '14px',
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ },
+ },
+ {
+ selector: `node[type=${occurrenceNode}]`,
+ style: {'background-color': '#1E90FF'},
+ },
+ {
+ selector: 'edge',
+ style: {
+ label: 'data(label)',
+ width: 3,
+ 'line-color': '#ccc',
+ 'curve-style': 'bezier',
+ 'target-arrow-color': 'black',
+ 'target-arrow-shape': 'triangle',
+ },
+ },
+ ],
+ layout: {
+ name: 'cose',
+ },
+});
+
+async function init() {
+ function createSublist(tableIndex, misconfigurations) {
+ const table = document.querySelector(`.misc-table[data-index = "${tableIndex}"]`);
+ const content = document.querySelector(`.content[data-index = "${tableIndex}"]`)
+ const allContent = document.querySelectorAll('.content');
+ const allTables = document.querySelectorAll('table');
+ const info = document.getElementById('info');
+
+ misconfigurations.forEach((misconfig, idx) => {
+ const listItem = document.querySelector(`[data-index="${tableIndex} ${idx}"]`);
+ listItem.addEventListener('click', function (e) {
+ e.stopPropagation();
+ currentTable = table;
+ if (!inWindow) {
+ changeContainer(tableIndex)
+ allTables.forEach((table) => {
+ table.style.width = "100%";
+
+ });
+
+ allContent.forEach((item) => {
+ item.style.display = "none";
+ });
+ info.style.position = 'absolute';
+
+ table.style.width = '70%';
+ content.style.display = 'flex';
+
+ removePlaceholderRows();
+ addPlaceholderRows();
+
+ } else {
+ info.style.position = 'fixed';
+ }
+
+ info.style.display = "none";
+ renderGraph(misconfig);
+ });
+
+ });
+
+ }
+
+ results.forEach((result, index) => {
+ if (result.Misconfigurations === undefined) {
+ return;
+ }
+
+ createSublist(index, result.Misconfigurations);
+
+ });
+}
+
+window.onload = init;
+
+function addPlaceholderRows() {
+ const rowCount = currentTable.querySelectorAll("tr").length;
+ const targetRows = 10;
+ const placeholderCount = targetRows - rowCount;
+
+ for (let i = 0; i < placeholderCount; i++) {
+ const placeholderRow = document.createElement("tr");
+ placeholderRow.classList.add("placeholder-row");
+ placeholderRow.innerHTML = " | ";
+ currentTable.querySelector("tbody").appendChild(placeholderRow);
+ }
+}
+
+function removePlaceholderRows() {
+ const placeholders = document.querySelectorAll(".placeholder-row");
+ placeholders.forEach(row => row.remove());
+}
+
+function changeContainer(index) {
+ const newContainer = document.querySelector(`.graph[data-index="${index}"]`);
+
+ const elements = cy.json().elements.nodes ? cy.json().elements : [];
+
+ const style = cy.style().json();
+
+ cy.destroy();
+
+ cy = cytoscape({
+ container: newContainer,
+ elements: elements,
+ style: style,
+ layout: {name: 'cose'}
+ });
+
+ addInfoEventsListening(index);
+}
+
+function addInfoEventsListening(index) {
+ cy.on('tap', 'node', function (evt) {
+ const node = evt.target;
+ const infoBox = document.getElementById("info");
+ const graphRect = document.querySelector(`.graph[data-index="${index}"]`).getBoundingClientRect();
+
+ let content = '';
+ if (node.data('type') === codeNode) {
+ content = `Code Snippet:
${node.data('content')}`;
+ } else if (node.data('type') === occurrenceNode) {
+ content = `File: ${node.data('file')}
Location: Lines ${node.data('start')} - ${node.data('end')}`;
+ }
+
+ let top = graphRect.top + evt.renderedPosition.y;
+ let left = window.scrollX + graphRect.left + evt.renderedPosition.x;
+
+ if (!inWindow) {
+ top += window.scrollY;
+ }
+
+ infoBox.innerHTML = content;
+ infoBox.style.left = left + 'px';
+ infoBox.style.top = top + 'px';
+ infoBox.style.display = 'block';
+ });
+
+ cy.on('tap', function (event) {
+ if (event.target === cy) {
+ document.getElementById("info").style.display = 'none';
+ }
+ });
+}
+
+function renderGraph(misconf) {
+ const nodes = [];
+ const edges = [];
+
+ if (misconf.CauseMetadata === undefined) {
+ return;
+ }
+
+ const findingID = `finding`;
+ if (misconf.CauseMetadata.Code?.Lines !== null) {
+ const code = misconf.CauseMetadata.Code?.Lines.reduce((prev, curr, idx) => {
+ if (idx === 0) {
+ return curr.Content;
+ }
+ return prev + '\n' + curr.Content;
+ }, '');
+
+ const highlightedCode = hljs.highlight(code, {language: 'hcl'}).value;
+
+ nodes.push({
+ data: {
+ id: findingID,
+ label: misconf.CauseMetadata.Resource,
+ type: codeNode,
+ content: highlightedCode,
+ },
+ });
+ }
+
+ if (misconf.CauseMetadata.Occurrences !== undefined) {
+ misconf.CauseMetadata.Occurrences.forEach((occurrence, idx) => {
+ const occurenceID = `${idx}-occurrence`;
+ nodes.push({
+ data: {
+ id: occurenceID,
+ label: occurrence.Resource,
+ type: occurrenceNode,
+ file: occurrence.Filename,
+ start: occurrence.Location.StartLine,
+ end: occurrence.Location.EndLine,
+ },
+ });
+
+ const edgeID = `${idx}-edge`;
+
+ // the first occurrence is the cause, which is stored in the cause metadata
+ if (idx === 0) {
+ edges.push({
+ data: {
+ id: edgeID,
+ source: occurenceID,
+ target: findingID,
+ label: 'Caused by',
+ },
+ });
+ } else {
+ const targetID = `${idx - 1}-occurrence`;
+
+ edges.push({
+ data: {
+ id: edgeID,
+ source: occurenceID,
+ target: targetID,
+ label: 'Caused by',
+ },
+ });
+ }
+ });
+ }
+ cy.elements().remove();
+ cy.add(nodes);
+ cy.add(edges);
+
+ cy.layout({name: 'cose'}).run();
+
+}
+
+
+const allTables = document.querySelectorAll('table');
+const closeContent = document.querySelectorAll('.close-button');
+const content = document.querySelectorAll('.content');
+const info = document.getElementById('info');
+const movableWindow = document.getElementById('movable-window');
+const hideButton = document.querySelector('.hide-window');
+const openWindow = document.querySelectorAll('.open-window-button');
+
+closeContent.forEach((button) => {
+ button.addEventListener('click', () => {
+ info.style.display = 'none';
+ allTables.forEach((table) => {
+ table.style.width = "100%";
+
+ });
+ removePlaceholderRows();
+ content.forEach((item) => {
+ item.style.display = "none";
+ });
+ });
+
+});
+
+function closeMovableWindow() {
+ movableWindow.style.display = 'none';
+ info.style.display = 'none';
+ inWindow = false;
+}
+
+document.querySelector('.close-window').addEventListener('click', () => {
+ closeMovableWindow();
+});
+
+hideButton.addEventListener("click", () => {
+ movableWindow.classList.toggle('hidden');
+ info.style.display = 'none';
+});
+
+openWindow.forEach((button) => {
+ button.addEventListener("click", () => {
+ if (inWindow) {
+ inWindow = false;
+ let index = currentTable.getAttribute('data-index');
+ let content = document.querySelector(`.content[data-index = "${index}"]`)
+ closeMovableWindow();
+ changeContainer(index);
+ currentTable.style.width = '70%';
+ info.style.display = 'none';
+ info.style.position = 'absolute';
+ content.style.display = 'flex';
+ } else {
+ inWindow = true;
+ changeContainer("window");
+ removePlaceholderRows();
+
+ allTables.forEach((table) => {
+ table.style.width = '100%';
+ });
+
+ document.querySelectorAll('.content').forEach((item) => {
+ item.style.display = "none";
+ });
+
+ info.style.position = 'fixed';
+ info.style.display = 'none';
+
+ movableWindow.style.display = 'block';
+ }
+
+ });
+});
+
+
+const header = document.getElementById('movable-header');
+
+let isDragging = false;
+let offsetX = 0;
+let offsetY = 0;
+
+header.addEventListener('mousedown', (e) => {
+ isDragging = true;
+ offsetX = e.clientX - movableWindow.offsetLeft;
+ offsetY = e.clientY - movableWindow.offsetTop;
+ document.body.style.userSelect = 'none';
+});
+
+document.addEventListener('mousemove', (e) => {
+ if (isDragging) {
+ let newLeft = e.clientX - offsetX;
+ let newTop = e.clientY - offsetY;
+
+ newLeft = Math.max(0, Math.min(window.innerWidth - movableWindow.offsetWidth, newLeft));
+ newTop = Math.max(0, Math.min(window.innerHeight - movableWindow.offsetHeight, newTop));
+
+ movableWindow.style.left = newLeft + 'px';
+ movableWindow.style.top = newTop + 'px';
+
+ cy.resize()
+ }
+});
\ No newline at end of file
diff --git a/render/templates/src/components/interactivity.js b/render/templates/src/components/interactivity.js
new file mode 100644
index 0000000..e6aa04c
--- /dev/null
+++ b/render/templates/src/components/interactivity.js
@@ -0,0 +1,139 @@
+document.addEventListener('mouseup', () => {
+ isDragging = false;
+ document.body.style.userSelect = '';
+});
+
+function insertAfter(referenceNode, newNode) {
+ referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
+}
+
+const severityOrder = {
+ UNKNOWN: {order: 5},
+ LOW: {order: 4},
+ MEDIUM: {order: 3},
+ HIGH: {order: 2},
+ CRITICAL: {order: 1},
+};
+
+function attachLinksInteractivity() {
+ document.querySelectorAll("td.links").forEach(function (linkCell) {
+ const links = [].concat.apply([], linkCell.querySelectorAll("a"));
+ [].sort.apply(links, function (a, b) {
+ return a.href > b.href ? 1 : -1;
+ });
+ links.forEach(function (link, idx) {
+ if (links.length > 0 && 0 === idx) {
+ const toggleLink = document.createElement("a");
+ toggleLink.innerText = "Toggle more links";
+ toggleLink.href = "#toggleMore";
+ toggleLink.setAttribute("class", "toggle-more-links");
+ linkCell.appendChild(toggleLink);
+ }
+ linkCell.appendChild(link);
+ });
+ });
+
+ document
+ .querySelectorAll("a.toggle-more-links")
+ .forEach(function (toggleLink) {
+ toggleLink.onclick = function () {
+ const expanded =
+ toggleLink.parentElement.getAttribute("data-more-links");
+ toggleLink.parentElement.setAttribute(
+ "data-more-links",
+ "on" === expanded ? "off" : "on"
+ );
+ return false;
+ };
+ });
+}
+
+function attachFilterInteractivity() {
+ const filterBar = document.querySelector(".filter_bar");
+ const nameFilter = filterBar.querySelector(".filter_bar__filter_name");
+ const filterable = document.querySelectorAll(".filterable");
+ const cellClasses = [
+ ".pkg-name",
+ ".vuln",
+ ".misc-type",
+ ".misc-id",
+ ".severity",
+ ".pkg-version",
+ ".pkg-key-name",
+ ".pkg-key-version",
+ ".pkg-key-src-name",
+ ".pkg-key-src-version",
+ ];
+
+ function applyFilters(filterValue) {
+ filterable.forEach((f) => {
+ const cellValues = cellClasses
+ .map((cl) => f.querySelector(cl))
+ .filter((cell) => cell !== null)
+ .map((cell) => cell.textContent || cell.innerText);
+
+ const condition = cellValues.some((cellValue) =>
+ cellValue.toUpperCase().includes(filterValue.toUpperCase())
+ );
+
+ f.style.display = condition ? "" : "none";
+ });
+ }
+
+ nameFilter.addEventListener("keyup", (e) => {
+ applyFilters(e.target.value);
+ });
+}
+
+function attachSortInteractivity() {
+ let colIx = -1;
+ const tables = document.querySelectorAll("table");
+ const sortTable = (tableIx, cellIndex, type, isSorded) => {
+ const table = tables[tableIx];
+ const tbody = table.querySelector('tbody[data-main="true"]');
+ const thead = table.querySelector("thead");
+ const inv = (val) => (isSorded ? -val : val);
+ const compare = (a, b) => {
+ if (!a.cells[cellIndex] || !b.cells[cellIndex]) return 0;
+ const rowA = a.cells[cellIndex].innerHTML;
+ const rowB = b.cells[cellIndex].innerHTML;
+ if (type === "string") {
+ if (rowA < rowB) return inv(-1);
+ if (rowA > rowB) return inv(1);
+ return 0;
+ }
+ if (type === "severity") {
+ const orderA = severityOrder[rowA].order;
+ const orderB = severityOrder[rowB].order;
+ if (orderA < orderB) return inv(-1);
+ if (orderA > orderB) return inv(1);
+ return 0;
+ }
+ };
+ let rows = Array(...tbody.rows);
+ rows.sort(compare);
+ table.removeChild(tbody);
+ rows.forEach((row) => {
+ tbody.appendChild(row);
+ });
+ insertAfter(thead, tbody);
+ };
+ tables.forEach((table, tableIx) => {
+ table.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const el = e.target;
+ const type = el.getAttribute("data-type");
+ const sortable = el.getAttribute("data-sortable") === "true";
+ if (el.nodeName !== "TH" || !sortable) return;
+ const cellIndex = el.cellIndex;
+ sortTable(tableIx, cellIndex, type, colIx === cellIndex);
+ colIx = colIx === cellIndex ? -1 : cellIndex;
+ });
+ });
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ attachLinksInteractivity();
+ attachSortInteractivity();
+ attachFilterInteractivity();
+});
\ No newline at end of file
diff --git a/render/templates/src/components/style.css b/render/templates/src/components/style.css
new file mode 100644
index 0000000..1f4e16a
--- /dev/null
+++ b/render/templates/src/components/style.css
@@ -0,0 +1,351 @@
+* {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+body {
+ height: 100vh;
+ width: 99vw;
+}
+
+h1, h2 {
+ text-align: center;
+}
+
+.top-vuln-title, .top-misc-title {
+ border-bottom: 1px solid #0000001f;
+}
+
+.initially-disabled {
+ display: none;
+}
+
+.link {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: 100%;
+ height: 1.2em;
+ white-space: nowrap;
+}
+
+.vuln {
+ word-wrap: anywhere;
+}
+
+.group-header th {
+ font-size: 200%;
+}
+
+table,
+th,
+td {
+ width: 100%;
+ border-top: 1px solid #0000001f;
+ border-collapse: collapse;
+ padding: .3em;
+ white-space: normal;
+}
+
+table {
+ margin-left: 0;
+ table-layout: fixed;
+ border: 2px solid #ddd;
+}
+
+.placeholder-row td {
+ height: 30px;
+ border: none;
+ background-color: transparent;
+ padding: 0;
+}
+
+.last-data-row td {
+ border-bottom: none;
+}
+
+.severity {
+ font-weight: bold;
+}
+
+
+table tr td:first-of-type {
+ font-weight: bold;
+}
+
+.links a,
+.links[data-more-links=on] a {
+ display: block;
+}
+
+.links[data-more-links=off] a:nth-of-type(1n+2) {
+ display: none;
+}
+
+a.toggle-more-links {
+ cursor: pointer;
+}
+
+th[data-sortable="true"] {
+ cursor: pointer;
+}
+
+.sub-header th {
+ font-size: 150%;
+ text-align: center;
+ background-color: #ddd;
+}
+
+th svg {
+ visibility: hidden;
+ pointer-events: none;
+}
+
+th span {
+ pointer-events: none;
+}
+
+.sub-header th[data-sortable="true"] svg {
+ visibility: visible;
+}
+
+ul {
+ list-style-type: none;
+}
+
+.search {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23bdbdbd' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'%3E%3C/path%3E%3C/svg%3E");
+ background-position: 10px 10px;
+ background-repeat: no-repeat;
+ width: 100%;
+ font-size: 16px;
+ padding: 12px 20px 12px 40px;
+ border: 1px solid #ddd;
+}
+
+.search:focus {
+ outline: none;
+ border-color: #07f;
+ box-shadow: 0 0 0 2px rgba(0, 119, 255, 0.2);
+}
+
+.filter_bar {
+ display: flex;
+ align-items: center;
+}
+
+.filterable:hover {
+ background-color: #f1f1f1;
+}
+
+.pkg-name {
+ width: 100%;
+ height: 1.2em;
+}
+
+.ta-center {
+ text-align: center;
+}
+
+.break-word {
+ word-wrap: break-word;
+}
+
+.header__title {
+ font-size: 24px;
+}
+
+.secret__line {
+ display: flex;
+}
+
+.secret__code {
+ background-color: #f1f1f1;
+ border: #e1e1e1;
+ border-radius: 5px;
+ padding: 10px 0 10px 15px;
+}
+
+.secret__line pre {
+ margin: 0;
+ overflow-wrap: anywhere;
+ white-space: pre-line;
+}
+
+.secret__line-cause {
+ color: #e40000;
+}
+
+.secret__line-number {
+ border-right: 1px solid black;
+ margin-right: 10px;
+ padding-right: 10px;
+}
+
+.secret__src-file {
+ color: #1f6feb
+}
+
+.secret__src-lines {
+ color: #1f6feb
+}
+
+.severity-LOW {
+ color: #5fbb31;
+}
+
+.severity-MEDIUM {
+ color: #e9c600;
+}
+
+.severity-HIGH {
+ color: #ff8800;
+}
+
+.severity-CRITICAL {
+ color: #e40000;
+}
+
+.severity-UNKNOWN {
+ color: #747474;
+}
+
+.secret__severity {
+ font-weight: bold;
+}
+
+.secret-results__title {
+ margin-bottom: 0;
+ font-size: 24px;
+}
+
+.secret__head {
+ margin-left: 15px;
+}
+
+.secret__title {
+ margin-left: 15px;
+}
+
+.secret__src {
+ margin-left: 15px;
+}
+
+.secret + .secret {
+ margin-top: 10px;
+}
+
+ul {
+ list-style: none;
+}
+
+.hidden {
+ display: none;
+}
+
+.target-item {
+ margin-bottom: 10px;
+}
+
+.target-item:hover {
+ background-color: #7d7d7d;
+}
+
+.sidebar li > ul {
+ display: none;
+ /* padding-left: 20px; */
+}
+
+.sidebar li.active > ul {
+ display: block;
+}
+
+.graph {
+ flex-grow: 1;
+ background-color: #ecf0f1;
+ width: 100%;
+ height: 100%;
+}
+
+.content {
+ flex-direction: column;
+ flex: 1 1 auto;
+ display: none;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ overflow: hidden;
+}
+
+.movable-content {
+ padding: 10px;
+ height: 100%;
+ background-color: #fff;
+ border: 1px solid #ccc;
+}
+
+.graph-buttons button,
+.open-window-button
+{
+ background: none;
+ border: none;
+ font-size: 18px;
+ color: white;
+ cursor: pointer;
+ font-weight: bold;
+}
+
+.graph-buttons{
+ flex-direction: row;
+ display: flex;
+}
+
+.content-header {
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ background-color: #007bff;
+ color: white;
+ padding: 10px;
+ user-select: none;
+}
+
+#movable-window {
+ position: fixed;
+ top: 2%;
+ right: 3%;
+ width: 30%;
+ height: 50%;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+ z-index: 1000;
+ display: none;
+}
+
+
+#movable-window.hidden {
+ height: auto;
+ min-height: unset;
+ overflow: hidden;
+}
+
+#movable-window.hidden .movable-content {
+ display: none;
+}
+
+.container {
+ padding-bottom: 10px;
+ flex-direction: row;
+ width: 100%;
+ display: flex;
+ overflow: hidden;
+}
+
+#info {
+ display: none;
+ z-index: 1000;
+ position: absolute;
+ background: white;
+ border: 1px solid black;
+ padding: 5px;
+ left: 599px;
+ top: 359px;
+}
\ No newline at end of file
diff --git a/render/templates/src/highlightjs/github.min.css b/render/templates/src/highlightjs/github.min.css
new file mode 100644
index 0000000..67184f5
--- /dev/null
+++ b/render/templates/src/highlightjs/github.min.css
@@ -0,0 +1,85 @@
+/*!
+ Theme: GitHub
+ Description: Light theme as seen on github.com
+ Author: github.com
+ Maintainer: @Hirse
+ Updated: 2021-05-15
+
+ Outdated base version: https://github.com/primer/github-syntax-light
+ Current colors taken from GitHub's CSS
+ */
+.hljs {
+ color: #24292e;
+ background: #fff
+}
+
+.hljs-doctag, .hljs-keyword, .hljs-meta .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_ {
+ color: #d73a49
+}
+
+.hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__, .hljs-title.function_ {
+ color: #6f42c1
+}
+
+.hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable {
+ color: #005cc5
+}
+
+.hljs-meta .hljs-string, .hljs-regexp, .hljs-string {
+ color: #032f62
+}
+
+.hljs-built_in, .hljs-symbol {
+ color: #e36209
+}
+
+.hljs-code, .hljs-comment, .hljs-formula {
+ color: #6a737d
+}
+
+.hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag {
+ color: #22863a
+}
+
+.hljs-subst {
+ color: #24292e
+}
+
+.hljs-section {
+ color: #005cc5;
+ font-weight: 700
+}
+
+.hljs-bullet {
+ color: #735c0f
+}
+
+.hljs-emphasis {
+ color: #24292e;
+ font-style: italic
+}
+
+.hljs-strong {
+ color: #24292e;
+ font-weight: 700
+}
+
+.hljs-addition {
+ color: #22863a;
+ background-color: #f0fff4
+}
+
+.hljs-deletion {
+ color: #b31d28;
+ background-color: #ffeef0
+}
+
+pre code.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1em
+}
+
+code.hljs {
+ padding: 3px 5px
+}
\ No newline at end of file
diff --git a/render/templates/src/highlightjs/highlight.min.js b/render/templates/src/highlightjs/highlight.min.js
new file mode 100644
index 0000000..9a95dbe
--- /dev/null
+++ b/render/templates/src/highlightjs/highlight.min.js
@@ -0,0 +1,309 @@
+/*!
+ Highlight.js v11.10.0 (git: 366a8bd012)
+ (c) 2006-2024 Josh Goebel and other contributors
+ License: BSD-3-Clause
+ */
+var hljs=function(){"use strict";function e(t){
+return t instanceof Map?t.clear=t.delete=t.set=()=>{
+throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
+throw Error("set is read-only")
+}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{
+const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i)
+})),t}class t{constructor(e){
+void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
+ignoreMatch(){this.isMatchIgnored=!0}}function n(e){
+return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")
+}function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
+;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope
+;class o{constructor(e,t){
+this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
+this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{
+if(e.startsWith("language:"))return e.replace("language:","language-")
+;if(e.includes(".")){const n=e.split(".")
+;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")
+}return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)}
+closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){
+this.buffer+=``}}const r=(e={})=>{const t={children:[]}
+;return Object.assign(t,e),t};class a{constructor(){
+this.rootNode=r(),this.stack=[this.rootNode]}get top(){
+return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
+this.top.children.push(e)}openNode(e){const t=r({scope:e})
+;this.add(t),this.stack.push(t)}closeNode(){
+if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
+for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
+walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
+return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
+t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
+"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
+a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e}
+addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){
+this.closeNode()}__addSublanguage(e,t){const n=e.root
+;t&&(n.scope="language:"+t),this.add(n)}toHTML(){
+return new o(this,this.options).value()}finalize(){
+return this.closeAllNodes(),!0}}function l(e){
+return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")}
+function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")}
+function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{
+const t=e[e.length-1]
+;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}
+})(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"}
+function p(e){return RegExp(e.toString()+"|").exec("").length-1}
+const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
+;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n
+;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break}
+s+=i.substring(0,e.index),
+i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0],
+"("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)}
+const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={
+begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'",
+illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",
+contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t,
+contains:[]},n);s.contains.push({scope:"doctag",
+begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
+end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
+;const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
+;return s.contains.push({begin:h(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s
+},S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({
+__proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{
+scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N,
+C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number",
+begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{
+"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
+t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E,
+MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0},
+NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w,
+PHRASAL_WORDS_MODE:{
+begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
+},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/,
+end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]},
+RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
+SHEBANG:(e={})=>{const t=/^#![ ]*\//
+;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t,
+end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},
+TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x,
+UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){
+"."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){
+void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){
+t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
+e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
+void 0===e.relevance&&(e.relevance=0))}function L(e,t){
+Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){
+if(e.match){
+if(e.begin||e.end)throw Error("begin & end are not supported with match")
+;e.begin=e.match,delete e.match}}function P(e,t){
+void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return
+;if(e.starts)throw Error("beforeMatch cannot be used with starts")
+;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]
+})),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={
+relevance:0,contains:[Object.assign(n,{endsParent:!0})]
+},e.relevance=0,delete n.beforeMatch
+},H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword"
+;function $(e,t,n=C){const i=Object.create(null)
+;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{
+Object.assign(i,$(e[n],t,n))})),i;function s(e,n){
+t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
+;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){
+return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{
+console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{
+z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0)
+},K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],o={},r={}
+;for(let e=1;e<=t.length;e++)r[e+i]=s[e],o[e+i]=!0,i+=p(t[e-1])
+;e[n]=r,e[n]._emit=o,e[n]._multi=!0}function Z(e){(e=>{
+e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
+delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
+_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
+}),(e=>{if(Array.isArray(e.begin)){
+if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
+K
+;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"),
+K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{
+if(Array.isArray(e.end)){
+if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
+K
+;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"),
+K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){
+function t(t,n){
+return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":""))
+}class n{constructor(){
+this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
+addRule(e,t){
+t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
+this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
+;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|"
+}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
+;const t=this.matcherRe.exec(e);if(!t)return null
+;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
+;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){
+this.rules=[],this.multiRegexes=[],
+this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
+if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n
+;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
+t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
+return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
+this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
+const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
+;let n=t.exec(e)
+;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
+const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
+return n&&(this.regexIndex+=n.position+1,
+this.regexIndex===this.count&&this.considerAll()),n}}
+if(e.compilerExtensions||(e.compilerExtensions=[]),
+e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
+;return e.classNameAliases=i(e.classNameAliases||{}),function n(o,r){const a=o
+;if(o.isCompiled)return a
+;[I,B,Z,D].forEach((e=>e(o,r))),e.compilerExtensions.forEach((e=>e(o,r))),
+o.__beforeBegin=null,[T,L,P].forEach((e=>e(o,r))),o.isCompiled=!0;let c=null
+;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords),
+c=o.keywords.$pattern,
+delete o.keywords.$pattern),c=c||/\w+/,o.keywords&&(o.keywords=$(o.keywords,e.case_insensitive)),
+a.keywordPatternRe=t(c,!0),
+r&&(o.begin||(o.begin=/\B|\b/),a.beginRe=t(a.begin),o.end||o.endsWithParent||(o.end=/\B|\b/),
+o.end&&(a.endRe=t(a.end)),
+a.terminatorEnd=l(a.end)||"",o.endsWithParent&&r.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+r.terminatorEnd)),
+o.illegal&&(a.illegalRe=t(o.illegal)),
+o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{
+variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{
+starts:e.starts?i(e.starts):null
+}):Object.isFrozen(e)?i(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,a)
+})),o.starts&&n(o.starts,r),a.matcher=(e=>{const t=new s
+;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
+}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
+}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){
+return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{
+constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}}
+const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{
+const i=Object.create(null),s=Object.create(null),o=[];let r=!0
+;const a="Could not find the language '{}', did you forget to load/include a language module?",l={
+disableAutodetect:!0,name:"Plain text",contains:[]};let p={
+ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
+languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
+cssSelector:"pre code",languages:null,__emitter:c};function b(e){
+return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s=""
+;"object"==typeof t?(i=e,
+n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."),
+G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
+s=e,i=t),void 0===n&&(n=!0);const o={code:i,language:s};N("before:highlight",o)
+;const r=o.result?o.result:E(o.language,o.code,n)
+;return r.code=o.code,N("after:highlight",r),r}function E(e,n,s,o){
+const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R)
+;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n=""
+;for(;t;){n+=R.substring(e,t.index)
+;const s=_.case_insensitive?t[0].toLowerCase():t[0],o=(i=s,N.keywords[i]);if(o){
+const[e,i]=o
+;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{
+const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0]
+;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i
+;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{
+if(""===R)return;let e=null;if("string"==typeof N.subLanguage){
+if(!i[N.subLanguage])return void M.addText(R)
+;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top
+}else e=x(R,N.subLanguage.length?N.subLanguage:null)
+;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language)
+})():l(),R=""}function u(e,t){
+""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1
+;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue}
+const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}}
+function h(e,t){
+return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope),
+e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
+R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{
+value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t)
+;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e)
+;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){
+for(;e.endsParent&&e.parent;)e=e.parent;return e}}
+if(e.endsWithParent)return f(e.parent,n,i)}function b(e){
+return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){
+const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const o=N
+;N.endScope&&N.endScope._wrap?(g(),
+u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(),
+d(N.endScope,e)):o.skip?R+=t:(o.returnEnd||o.excludeEnd||(R+=t),
+g(),o.excludeEnd&&(R=t));do{
+N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent
+}while(N!==s.parent);return s.starts&&h(s.starts,e),o.returnEnd?0:t.length}
+let w={};function y(i,o){const a=o&&o[0];if(R+=i,null==a)return g(),0
+;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===a){
+if(R+=n.slice(o.index,o.index+1),!r){const t=Error(`0 width match regex (${e})`)
+;throw t.languageName=e,t.badRule=w.rule,t}return 1}
+if(w=o,"begin"===o.type)return(e=>{
+const n=e[0],i=e.rule,s=new t(i),o=[i.__beforeBegin,i["on:begin"]]
+;for(const t of o)if(t&&(t(e,s),s.isMatchIgnored))return b(n)
+;return i.skip?R+=n:(i.excludeBegin&&(R+=n),
+g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(o)
+;if("illegal"===o.type&&!s){
+const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"')
+;throw e.mode=N,e}if("end"===o.type){const e=m(o);if(e!==ee)return e}
+if("illegal"===o.type&&""===a)return 1
+;if(I>1e5&&I>3*o.index)throw Error("potential infinite loop, way more iterations than matches")
+;return R+=a,a.length}const _=O(e)
+;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"')
+;const v=V(_);let k="",N=o||v;const S={},M=new p.__emitter(p);(()=>{const e=[]
+;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope)
+;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{
+if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){
+I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A
+;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(A,e.index),e)
+;A=e.index+t}y(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e,
+value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){
+if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n),
+illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A,
+context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(r)return{
+language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N}
+;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{
+const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)}
+;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1)))
+;s.unshift(n);const o=s.sort(((e,t)=>{
+if(e.relevance!==t.relevance)return t.relevance-e.relevance
+;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1
+;if(O(t.language).supersetOf===e.language)return-1}return 0})),[r,a]=o,c=r
+;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{
+let t=e.className+" ";t+=e.parentNode?e.parentNode.className:""
+;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1])
+;return t||(X(a.replace("{}",n[1])),
+X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}
+return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return
+;if(N("before:highlightElement",{el:e,language:n
+}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e)
+;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
+console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
+console.warn("The element with unescaped HTML:"),
+console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML)
+;t=e;const i=t.textContent,o=n?m(i,{language:n,ignoreIllegals:!0}):x(i)
+;e.innerHTML=o.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n
+;e.classList.add("hljs"),e.classList.add("language-"+i)
+})(e,n,o.language),e.result={language:o.language,re:o.relevance,
+relevance:o.relevance},o.secondBest&&(e.secondBest={
+language:o.secondBest.language,relevance:o.secondBest.relevance
+}),N("after:highlightElement",{el:e,result:o,text:i})}let y=!1;function _(){
+"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0
+}function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}
+function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
+s[e.toLowerCase()]=t}))}function k(e){const t=O(e)
+;return t&&!t.disableAutodetect}function N(e,t){const n=e;o.forEach((e=>{
+e[n]&&e[n](t)}))}
+"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
+y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_,
+highlightElement:w,
+highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"),
+G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)},
+initHighlighting:()=>{
+_(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
+initHighlightingOnLoad:()=>{
+_(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
+},registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){
+if(W("Language definition for '{}' could not be registered.".replace("{}",e)),
+!r)throw t;W(t),s=l}
+s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{
+languageName:e})},unregisterLanguage:e=>{delete i[e]
+;for(const t of Object.keys(s))s[t]===e&&delete s[t]},
+listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v,
+autoDetection:k,inherit:Q,addPlugin:e=>{(e=>{
+e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
+e["before:highlightBlock"](Object.assign({block:t.el},t))
+}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
+e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),o.push(e)},
+removePlugin:e=>{const t=o.indexOf(e);-1!==t&&o.splice(t,1)}}),n.debugMode=()=>{
+r=!1},n.safeMode=()=>{r=!0},n.versionString="11.10.0",n.regex={concat:h,
+lookahead:g,either:f,optional:d,anyNumberOfTimes:u}
+;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n
+},ne=te({});return ne.newInstance=()=>te({}),ne}()
+;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);
\ No newline at end of file
diff --git a/render/templates/src/highlightjs/terraform.js b/render/templates/src/highlightjs/terraform.js
new file mode 100644
index 0000000..179c119
--- /dev/null
+++ b/render/templates/src/highlightjs/terraform.js
@@ -0,0 +1,87 @@
+/*
+ * highlight.js terraform syntax highlighting definition
+ *
+ * @see https://github.com/highlightjs/highlight.js
+ *
+ * :TODO:
+ *
+ * @package: highlightjs-terraform
+ * @author: Nikos Tsirmirakis
+ * @since: 2019-03-20
+ *
+ * Description: Terraform (HCL) language definition
+ * Category: scripting
+ */
+
+var module = module ? module : {}; // shim for browser use
+
+function hljsDefineTerraform(hljs) {
+ var NUMBERS = {
+ className: 'number',
+ begin: '\\b\\d+(\\.\\d+)?',
+ relevance: 0
+ };
+ var STRINGS = {
+ className: 'string',
+ begin: '"',
+ end: '"',
+ contains: [{
+ className: 'variable',
+ begin: '\\${',
+ end: '\\}',
+ relevance: 9,
+ contains: [{
+ className: 'string',
+ begin: '"',
+ end: '"'
+ }, {
+ className: 'meta',
+ begin: '[A-Za-z_0-9]*' + '\\(',
+ end: '\\)',
+ contains: [
+ NUMBERS, {
+ className: 'string',
+ begin: '"',
+ end: '"',
+ contains: [{
+ className: 'variable',
+ begin: '\\${',
+ end: '\\}',
+ contains: [{
+ className: 'string',
+ begin: '"',
+ end: '"',
+ contains: [{
+ className: 'variable',
+ begin: '\\${',
+ end: '\\}'
+ }]
+ }, {
+ className: 'meta',
+ begin: '[A-Za-z_0-9]*' + '\\(',
+ end: '\\)'
+ }]
+ }]
+ },
+ 'self']
+ }]
+ }]
+ };
+
+ return {
+ aliases: ['tf', 'hcl'],
+ keywords: 'resource variable provider output locals module data terraform|10',
+ literal: 'false true null',
+ contains: [
+ hljs.COMMENT('\\#', '$'),
+ NUMBERS,
+ STRINGS
+ ]
+ }
+}
+
+module.exports = function (hljs) {
+ hljs.registerLanguage('terraform', hljsDefineTerraform);
+};
+
+module.exports.definer = hljsDefineTerraform;
\ No newline at end of file
diff --git a/render/testdata/golden/empty.html b/render/testdata/golden/empty.html
index d5cab46..f31f7b4 100644
--- a/render/testdata/golden/empty.html
+++ b/render/testdata/golden/empty.html
@@ -4,493 +4,1342 @@
Trivy Report
-
+
+
+ Trivy Returned Empty Report
+
+
+
+
+
+
+ cy.resize()
+ }
+});
+ document.addEventListener('mouseup', () => {
+ isDragging = false;
+ document.body.style.userSelect = '';
+});
-
-
-
-
- Trivy Returned Empty Report
-
+ insertAfter(thead, tbody);
+ };
+ tables.forEach((table, tableIx) => {
+ table.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const el = e.target;
+ const type = el.getAttribute("data-type");
+ const sortable = el.getAttribute("data-sortable") === "true";
+ if (el.nodeName !== "TH" || !sortable) return;
+ const cellIndex = el.cellIndex;
+ sortTable(tableIx, cellIndex, type, colIx === cellIndex);
+ colIx = colIx === cellIndex ? -1 : cellIndex;
+ });
+ });
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ attachLinksInteractivity();
+ attachSortInteractivity();
+ attachFilterInteractivity();
+});
+