Skip to content

Instantly share code, notes, and snippets.

@mjervis-mo
Created September 9, 2025 09:41
Show Gist options
  • Select an option

  • Save mjervis-mo/a8bcddf5b94155ee1cdfc53b873a0408 to your computer and use it in GitHub Desktop.

Select an option

Save mjervis-mo/a8bcddf5b94155ee1cdfc53b873a0408 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name CVE to Ubuntu Link Converter
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Finds CVE numbers in brackets, like [CVE-2023-12345], and converts them to links to the Ubuntu CVE tracker.
// @author Gemini
// @match https://scanner.appcheck-ng.com/scans/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=appcheck-ng.com
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// Regex looks for CVEs inside parentheses, e.g., (CVE-2024-12345)
const cveRegex = /\((CVE-\d{4}-\d{4,})\)/gi;
const ubuntuUrlBase = 'https://ubuntu.com/security/';
/**
* Traverses a given DOM node, finds text nodes containing a CVE pattern,
* and replaces them with a styled, clickable link.
* @param {Node} node - The DOM node to process.
*/
function linkifyCVEsInNode(node) {
// Use a TreeWalker to efficiently find all text nodes within the given node.
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
const nodesToProcess = [];
// Collect all matching text nodes first to avoid modifying the DOM while iterating.
let currentNode;
while ((currentNode = walker.nextNode())) {
// Ignore text inside scripts, styles, or existing links.
if (currentNode.parentElement.tagName.match(/^(A|SCRIPT|STYLE)$/i)) {
continue;
}
if (cveRegex.test(currentNode.nodeValue)) {
nodesToProcess.push(currentNode);
}
}
// Now, process each of the collected text nodes.
nodesToProcess.forEach(textNode => {
// Ensure the node is still attached to the DOM before processing.
const parent = textNode.parentNode;
if (!parent) return;
const fragment = document.createDocumentFragment();
const text = textNode.nodeValue;
let lastIndex = 0;
let match;
cveRegex.lastIndex = 0; // Reset regex state before exec loop.
while ((match = cveRegex.exec(text)) !== null) {
// Add the text that comes before the CVE match.
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
const cveId = match[1]; // Captured CVE ID, e.g., "CVE-2023-12345"
const fullMatch = match[0]; // Full string match, e.g., "(CVE-2023-12345)"
// Create the new <a> element.
const link = document.createElement('a');
link.href = `${ubuntuUrlBase}${cveId}`;
link.textContent = fullMatch;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.style.color = '#E95420';
link.style.fontWeight = 'bold';
link.style.textDecoration = 'underline';
// Stop the click from triggering parent UI events.
link.addEventListener('click', (e) => e.stopPropagation());
fragment.appendChild(link);
lastIndex = cveRegex.lastIndex;
}
// Add any remaining text that came after the last CVE match.
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
// Replace the original text node with our new fragment.
parent.replaceChild(fragment, textNode);
});
}
// --- Main execution ---
// 1. Run an initial scan on the whole page for any content present at load time.
linkifyCVEsInNode(document.body);
// 2. Create a single, robust observer to watch for any nodes added to the page later.
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
// We only care about mutations where nodes were added.
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
// Only process element nodes (their text nodes will be found by the walker).
if (node.nodeType === Node.ELEMENT_NODE) {
linkifyCVEsInNode(node);
}
});
}
}
});
// 3. Start observing the entire document body for additions to its subtree.
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment