Last active
October 26, 2025 14:55
-
-
Save chocolateimage/ac7aad0b0b94e4ddb42b7847fa9cc35b to your computer and use it in GitHub Desktop.
A nice GitLab SAST Viewer.
This file contains hidden or 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
| // ==UserScript== | |
| // @name GitLab SAST Viewer | |
| // @namespace https://github.com/chocolateimage | |
| // @version 1.1.5 | |
| // @description Show SAST reports inline | |
| // @author chocolateimage | |
| // @match *://*/**/-/jobs/* | |
| // @match *://*/**/-/merge_requests/* | |
| // @match *://*/**/-/pipelines/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=gitlab.com | |
| // @grant GM.xmlHttpRequest | |
| // ==/UserScript== | |
| (async function() { | |
| 'use strict'; | |
| const window = unsafeWindow // unsafeWindow is required for accessing variables inside of the window | |
| if (!window.gl) return; | |
| function getProjectPath() { | |
| return location.pathname.substring(1).split("/-")[0] | |
| } | |
| function getJobId() { | |
| if (location.pathname.includes("/-/jobs/")) { | |
| return parseInt(location.pathname.split("/-/jobs/")[1].split("/")[0]) | |
| } else if ((location.pathname.includes("/-/pipelines/"))) { | |
| for (const job of document.querySelectorAll("[data-testid='ci-job-item-content']")) { | |
| if (job.textContent.includes("semgrep-sast") || job.textContent.includes("gitlab-advanced-sast")) { | |
| return parseInt(job.href.split("/-/jobs/")[1].split("/")[0]) | |
| } | |
| } | |
| } | |
| } | |
| function getCommitSHA() { | |
| return document.querySelector("[data-testid='commit-sha']").innerText ?? document.querySelector("[data-testid='commit-link']").innerText | |
| } | |
| async function installMarked() { | |
| if (window.marked != null) return; | |
| const scriptElement = document.createElement("script") | |
| scriptElement.src = "https://cdn.jsdelivr.net/npm/marked/marked.min.js" | |
| document.head.appendChild(scriptElement) | |
| while (window.marked == null) { | |
| await new Promise((r) => setTimeout(r, 10)) | |
| } | |
| } | |
| async function installDOMPurify() { | |
| if (window.DOMPurify != null) return; | |
| const scriptElement = document.createElement("script") | |
| scriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.6/purify.min.js" | |
| document.head.appendChild(scriptElement) | |
| while (window.DOMPurify == null) { | |
| await new Promise((r) => setTimeout(r, 10)) | |
| } | |
| } | |
| async function getReport(jobId) { | |
| const url = `/${getProjectPath()}/-/jobs/${jobId}/artifacts/download?file_type=sast`; | |
| console.log("Trying to get report at", url); | |
| return new Promise((resolve) => { | |
| GM.xmlHttpRequest({ | |
| method: "GET", | |
| url: url, | |
| onload: function (response) { | |
| if (response.status != 200) { | |
| console.error( | |
| "Something went wrong getting the SAST Report. Status code " + | |
| response.status, response.responseText | |
| ); | |
| return; | |
| } | |
| resolve(JSON.parse(response.responseText)) | |
| }, | |
| }); | |
| }); | |
| } | |
| async function viewSASTReport() { | |
| document.getElementById("viewSastReport").textContent = "Hide SAST Report" | |
| const existingSastVulnsElement = document.getElementById("sastVulnsList") | |
| if (existingSastVulnsElement != null) { | |
| if (existingSastVulnsElement.style.display == "none") { | |
| existingSastVulnsElement.style.display = "" | |
| document.querySelector(".build-log-container").style.display = "none" | |
| document.scrollingElement.scrollTop = 0 | |
| } else { | |
| existingSastVulnsElement.style.display = "none" | |
| document.querySelector(".build-log-container").style.display = "" | |
| document.getElementById("viewSastReport").textContent = "View SAST Report" | |
| } | |
| return; | |
| } | |
| const report = (await Promise.all([getReport(getJobId()), installMarked(), installDOMPurify()]))[0] | |
| document.querySelector(".build-log-container").style.display = "none" | |
| const vulnsElement = document.createElement("ul") | |
| vulnsElement.id = "sastVulnsList" | |
| vulnsElement.className = "content-list issuable-list issues-list" | |
| for (const vuln of report.vulnerabilities) { | |
| const detailsElement = document.createElement("div") | |
| detailsElement.style.padding = "12px 24px" | |
| detailsElement.style.display = "none" | |
| detailsElement.style.borderBottom = "1px solid var(--gl-border-color-default, var(--gl-color-neutral-100, #dcdcde))" | |
| const identifiersElement = document.createElement("div") | |
| identifiersElement.style.marginBottom = "6px" | |
| identifiersElement.style.display = "flex" | |
| identifiersElement.style.gap = "4px" | |
| identifiersElement.style.flexWrap = "wrap" | |
| for (const identifier of vuln.identifiers) { | |
| if (!["cwe", "owasp"].includes(identifier.type)) continue; | |
| const identifierElement = document.createElement(identifier.url != null ? "a" : "span") | |
| identifierElement.className = "gl-badge badge badge-pill" | |
| if (identifier.url != null) { | |
| identifierElement.href = identifier.url | |
| identifierElement.target = "_blank" | |
| identifierElement.classList.add("badge-info") | |
| } else { | |
| identifierElement.classList.add("badge-secondary") | |
| } | |
| identifierElement.textContent = identifier.name | |
| identifiersElement.appendChild(identifierElement) | |
| } | |
| detailsElement.appendChild(identifiersElement) | |
| const sourceElement = document.createElement("div") | |
| sourceElement.style.marginBottom = "8px" | |
| const sourceLinkElement = document.createElement("a") | |
| sourceLinkElement.target = "_blank" | |
| sourceLinkElement.href = `/${getProjectPath()}/-/blob/${getCommitSHA()}/${vuln.location.file}#L${vuln.location.start_line}` | |
| sourceLinkElement.textContent = `Source: ${vuln.location.file} Line ${vuln.location.start_line}` | |
| if (vuln.location.end_line != null) { | |
| sourceLinkElement.href += "-" + vuln.location.end_line | |
| sourceLinkElement.textContent += " - " + vuln.location.end_line | |
| } | |
| sourceElement.appendChild(sourceLinkElement) | |
| detailsElement.appendChild(sourceElement) | |
| const descriptionElement = document.createElement("div") | |
| descriptionElement.innerHTML = window.DOMPurify.sanitize(window.marked.parse(vuln.description).replaceAll("<code>","").replaceAll("</code>","")) | |
| detailsElement.appendChild(descriptionElement) | |
| const vulnElement = document.createElement("li") | |
| vulnElement.className = "issue !gl-flex !gl-px-5 issue-clickable gl-relative gl-cursor-pointer hover:gl-bg-subtle" | |
| const severityColors = { | |
| Info: "transparent", | |
| Low: "#808080", | |
| Warning: "#fb923c", | |
| Medium: "#fb923c", | |
| Error: "#f87171", | |
| High: "#f87171", | |
| Critical: "#ef4444", | |
| } | |
| vulnElement.style.borderLeft = "3px solid " + severityColors[vuln.severity] | |
| detailsElement.style.borderLeft = "3px solid " + severityColors[vuln.severity] | |
| const severityElement = document.createElement("span") | |
| severityElement.style.fontWeight = "600" | |
| severityElement.style.width = "100px" | |
| severityElement.textContent = vuln.severity | |
| vulnElement.appendChild(severityElement) | |
| const labelElement = document.createElement("span") | |
| labelElement.textContent = vuln.name | |
| vulnElement.appendChild(labelElement) | |
| vulnElement.addEventListener("click", (event) => { | |
| if (event.altKey) return | |
| if (detailsElement.style.display == "none") { | |
| detailsElement.style.display = "block" | |
| vulnElement.style.borderBottom = "none" | |
| } else { | |
| detailsElement.style.display = "none" | |
| vulnElement.style.borderBottom = "" | |
| } | |
| }) | |
| vulnsElement.appendChild(vulnElement) | |
| vulnsElement.appendChild(detailsElement) | |
| } | |
| if (report.vulnerabilities.length == 0) { | |
| const noVulnsElement = document.createElement("div") | |
| noVulnsElement.textContent = "No vulnerabilities found" | |
| noVulnsElement.style.marginTop = "16px" | |
| noVulnsElement.style.fontWeight = "600" | |
| noVulnsElement.style.fontSize = "20px" | |
| vulnsElement.appendChild(noVulnsElement) | |
| } | |
| document.querySelector(".build-page").appendChild(vulnsElement) | |
| document.scrollingElement.scrollTop = 0 | |
| } | |
| async function addSASTViewButton() { | |
| let jobNameElement = null; | |
| while (jobNameElement == null) { | |
| jobNameElement = document.querySelector("[data-testid='job-name']") | |
| await new Promise((r) => setTimeout(r, 50)) | |
| } | |
| if (jobNameElement.textContent.trim() != "semgrep-sast" && jobNameElement.textContent.trim() != "gitlab-advanced-sast") { | |
| return | |
| } | |
| let buildDetailRow = null; | |
| while (buildDetailRow == null) { | |
| buildDetailRow = document.querySelector(".build-detail-row") | |
| await new Promise((r) => setTimeout(r, 50)) | |
| } | |
| const viewButton = document.createElement("button") | |
| viewButton.id = "viewSastReport" | |
| viewButton.className = "btn btn-confirm gl-button" | |
| viewButton.style.display = "block" | |
| viewButton.style.width = "100%" | |
| viewButton.style.marginTop = "8px" | |
| viewButton.textContent = "View SAST Report" | |
| viewButton.addEventListener("click", viewSASTReport) | |
| buildDetailRow.parentElement.appendChild(viewButton) | |
| const searchParams = new URLSearchParams(location.search) | |
| if (searchParams.get("view-sast-report") == "true") { | |
| viewSASTReport() | |
| } | |
| } | |
| async function loadOnMergeRequest(retryAttempt = 0) { | |
| const securityScanElement = document.querySelectorAll("[data-testid='widget-extension-top-level']").values().find((element) => { | |
| return element.textContent.includes("Security scan") | |
| }) | |
| if (securityScanElement == null) { | |
| if (retryAttempt < 20) { | |
| setTimeout(() => loadOnMergeRequest(retryAttempt + 1), 500); | |
| } | |
| return | |
| } | |
| const fullReportButton = document.createElement("button") | |
| fullReportButton.className = "btn btn-default btn-sm gl-button" | |
| fullReportButton.style.marginLeft = "8px" | |
| fullReportButton.textContent = "View full report" | |
| fullReportButton.addEventListener("click", () => { | |
| window.location = document.querySelector("[data-testid='pipeline-id']").href + "?view-sast-report=true" | |
| }) | |
| securityScanElement.appendChild(fullReportButton) | |
| } | |
| function pipelineViewSASTReport(retryAttempt = 0) { | |
| const jobId = getJobId() | |
| if (jobId == null) { | |
| if (retryAttempt < 100) { | |
| setTimeout(() => pipelineViewSASTReport(retryAttempt + 1), 20); | |
| } | |
| return | |
| } | |
| window.location = `/${getProjectPath()}/-/jobs/${jobId}?view-sast-report=true` | |
| } | |
| function loadOnPipeline() { | |
| const searchParams = new URLSearchParams(location.search) | |
| if (searchParams.get("view-sast-report") == "true") { | |
| pipelineViewSASTReport() | |
| } | |
| } | |
| if (location.pathname.includes("/-/jobs/")) { | |
| addSASTViewButton() | |
| } else if (location.pathname.includes("/-/merge_requests/")) { | |
| loadOnMergeRequest() | |
| } else if (location.pathname.includes("/-/pipelines/")) { | |
| loadOnPipeline() | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment