Skip to content

Instantly share code, notes, and snippets.

@chocolateimage
Last active October 26, 2025 14:55
Show Gist options
  • Select an option

  • Save chocolateimage/ac7aad0b0b94e4ddb42b7847fa9cc35b to your computer and use it in GitHub Desktop.

Select an option

Save chocolateimage/ac7aad0b0b94e4ddb42b7847fa9cc35b to your computer and use it in GitHub Desktop.
A nice GitLab SAST Viewer.
// ==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