VirusTotal Engine Highlighter

Enhance VirusTotal file detection pages by highlighting specific engines

// ==UserScript==
// @name         VirusTotal Engine Highlighter
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Enhance VirusTotal file detection pages by highlighting specific engines
// @author       WUHUA
// @match        https://www.virustotal.com/gui/file/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      GPL
// ==/UserScript==

(function () {
    'use strict';

    // Only run on file details or detection pages
    if (!window.location.href.match(/\/gui\/file\/[a-f0-9]+(\/?|\/detection)/i)) {
        return;
    }

    // Default configuration with reduced initial engines list
    const defaultConfig = {
        selectedEngines: [
            "Microsoft",
            "Kaspersky",
            "CrowdStrike Falcon",
            "TrendMicro",
            "ESET-NOD32",
        ],
        highlightColor: "#FFFF99", // Light yellow
        darkModeHighlightColor: "#665500", // Dark yellow
        reorderEngines: true // Move highlighted engines to top
    };

    // Load configuration
    let config = GM_getValue('vtEnhanceConfig', defaultConfig);
    let shadowRootsCache = new WeakMap(); // Cache for shadow roots
    let pendingHighlightTask = null;

    // Save configuration function
    function saveConfig() {
        GM_setValue('vtEnhanceConfig', config);
    }

    function isDarkMode() {
        try {
            const vtColorMode = localStorage.getItem('colorMode');
            if (vtColorMode) return vtColorMode === 'dark';
        } catch (e) { }

        return document.body.classList.contains('dark-theme') ||
            document.documentElement.classList.contains('dark') ||
            window.matchMedia('(prefers-color-scheme: dark)').matches;
    }

    // Apply base styles once
    function applyBaseStyles() {
        const darkMode = isDarkMode();
        const highlightColor = darkMode ? config.darkModeHighlightColor : config.highlightColor;

        GM_addStyle(`
            div.detection.vt-enhanced-engine {
                background-color: ${highlightColor} !important;
                box-shadow: 0 0 4px rgba(0,0,0,0.2) !important;
                border-radius: 4px !important;
                padding: 2px !important;
                margin: 2px 0 !important;
            }

            vt-ui-detections-list vt-ui-expandable.vt-enhanced-engine {
                background-color: ${highlightColor} !important;
                border-radius: 4px !important;
                margin: 2px 0 !important;
            }

            .vt-enhanced-engine span,
            .vt-enhanced-engine div {
                color: ${darkMode ? '#000' : '#000'} !important;
                font-weight: bold !important;
            }

            #vt-enhance-toggle {
                position: fixed;
                bottom: 20px;
                left: 20px;
                background: #4CAF50;
                color: white;
                border: none;
                padding: 5px 10px;
                cursor: pointer;
                z-index: 9999;
                border-radius: 4px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            }

            #vt-enhance-config {
                position: fixed;
                bottom: 50px;
                left: 20px;
                background: ${darkMode ? '#222' : 'white'};
                color: ${darkMode ? '#eee' : '#333'};
                border: 1px solid ${darkMode ? '#444' : '#ccc'};
                padding: 15px;
                z-index: 9999;
                box-shadow: 0 0 10px rgba(0,0,0,${darkMode ? '0.5' : '0.2'});
                max-height: 80vh;
                overflow-y: auto;
                display: none;
                border-radius: 5px;
                width: 320px;
            }

            .vt-enhance-section { margin-bottom: 15px; }

            .vt-enhance-button {
                background: ${darkMode ? '#444' : '#f0f0f0'};
                color: ${darkMode ? '#eee' : '#333'};
                border: 1px solid ${darkMode ? '#666' : '#ccc'};
                padding: 5px 10px;
                margin-right: 5px;
                cursor: pointer;
                border-radius: 3px;
            }

            .vt-enhance-save {
                background: #4CAF50;
                color: white;
                border: none;
            }

            .vt-enhance-quick-toggle {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 18px;
                height: 18px;
                margin-left: 6px;
                border-radius: 3px;
                cursor: pointer;
                font-size: 12px;
                background-color: ${darkMode ? '#333' : '#eee'};
                color: ${darkMode ? '#aaa' : '#666'};
                border: 1px solid ${darkMode ? '#555' : '#ccc'};
                opacity: 0.7;
                transition: all 0.2s ease;
            }

            .vt-enhance-quick-toggle.active {
                background-color: #4CAF50;
                color: white;
                opacity: 1;
            }

            .vt-enhance-quick-toggle:hover {
                opacity: 1;
                transform: scale(1.1);
            }
        `);
    }

    // Create simplified UI
    function createConfigUI() {
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'vt-enhance-toggle';
        toggleBtn.textContent = 'VT Enhance';
        document.body.appendChild(toggleBtn);

        const configPanel = document.createElement('div');
        configPanel.id = 'vt-enhance-config';

        configPanel.innerHTML = `
            <h3>VirusTotal Engine Highlighter</h3>
            <p>Click ★ next to engine names to toggle highlighting</p>
            <div class="vt-enhance-section">
                <label><strong>Light Mode Color:</strong></label>
                <input type="color" id="vt-enhance-color" value="${config.highlightColor}">
            </div>
            <div class="vt-enhance-section">
                <label><strong>Dark Mode Color:</strong></label>
                <input type="color" id="vt-enhance-dark-color" value="${config.darkModeHighlightColor}">
            </div>
            <div class="vt-enhance-section">
                <label>
                    <input type="checkbox" id="vt-enhance-reorder" ${config.reorderEngines ? 'checked' : ''}>
                    <strong>Move highlighted to top</strong>
                </label>
            </div>
            <div class="vt-enhance-section">
                <button id="vt-enhance-save" class="vt-enhance-button vt-enhance-save">Save</button>
                <button id="vt-enhance-close" class="vt-enhance-button">Close</button>
            </div>
            <div class="vt-enhance-section">
                <button id="vt-enhance-export" class="vt-enhance-button">Export</button>
                <button id="vt-enhance-import" class="vt-enhance-button">Import</button>
            </div>
        `;

        document.body.appendChild(configPanel);

        // Event handlers
        toggleBtn.addEventListener('click', () => {
            const isVisible = configPanel.style.display === 'block';
            configPanel.style.display = isVisible ? 'none' : 'block';
        });

        document.getElementById('vt-enhance-save').addEventListener('click', saveSettings);
        document.getElementById('vt-enhance-close').addEventListener('click', () => {
            configPanel.style.display = 'none';
        });

        document.getElementById('vt-enhance-export').addEventListener('click', exportConfig);
        document.getElementById('vt-enhance-import').addEventListener('click', importConfig);
    }

    // Save settings function
    function saveSettings() {
        config.highlightColor = document.getElementById('vt-enhance-color').value;
        config.darkModeHighlightColor = document.getElementById('vt-enhance-dark-color').value;
        config.reorderEngines = document.getElementById('vt-enhance-reorder').checked;

        saveConfig();
        applyBaseStyles();
        scheduleHighlighting();

        document.getElementById('vt-enhance-config').style.display = 'none';

        // Show confirmation
        const notification = document.createElement('div');
        notification.textContent = 'Settings saved!';
        notification.style =
            'position:fixed; top:50px; right:20px; background:#4CAF50; ' +
            'color:white; padding:10px; border-radius:5px; z-index:10000;';
        document.body.appendChild(notification);
        setTimeout(() => notification.remove(), 2000);
    }

    // Config import/export
    function exportConfig() {
        const configStr = JSON.stringify(config);
        const blob = new Blob([configStr], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = 'vt_enhance_config.json';
        a.click();

        URL.revokeObjectURL(url);
    }

    function importConfig() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = e => {
            const file = e.target.files[0];
            const reader = new FileReader();
            reader.onload = function (e) {
                try {
                    const importedConfig = JSON.parse(e.target.result);
                    if (importedConfig.selectedEngines && importedConfig.highlightColor) {
                        config = importedConfig;
                        saveConfig();
                        updateUIFromConfig();
                        scheduleHighlighting();
                        alert('Configuration imported successfully!');
                    } else {
                        alert('Invalid configuration file!');
                    }
                } catch (error) {
                    alert('Error importing configuration: ' + error.message);
                }
            };
            reader.readAsText(file);
        };
        input.click();
    }

    function updateUIFromConfig() {
        if (document.getElementById('vt-enhance-color')) {
            document.getElementById('vt-enhance-color').value = config.highlightColor;
            document.getElementById('vt-enhance-dark-color').value = config.darkModeHighlightColor;
            document.getElementById('vt-enhance-reorder').checked = config.reorderEngines;
        }
    }

    // Optimized Shadow DOM query function with caching
    function queryShadowDOM(selector, root = document.body, maxDepth = 10) {
        if (maxDepth <= 0) return [];

        // Check cache first for this root
        if (shadowRootsCache.has(root)) {
            const cachedResults = shadowRootsCache.get(root).querySelectorAll(selector);
            if (cachedResults.length > 0) {
                return [...cachedResults];
            }
        }

        const results = [...root.querySelectorAll(selector)];

        // Find elements with shadow roots
        const elementsWithShadow = [...root.querySelectorAll('*')]
            .filter(el => el.shadowRoot)
            .map(el => el.shadowRoot);

        // Cache shadow roots for future queries
        elementsWithShadow.forEach(shadowRoot => {
            if (!shadowRootsCache.has(shadowRoot)) {
                shadowRootsCache.set(shadowRoot, shadowRoot);
            }

            // Query inside shadow root
            results.push(...shadowRoot.querySelectorAll(selector));

            // Recursively search deeper, but limit depth
            results.push(...queryShadowDOM(selector, shadowRoot, maxDepth - 1));
        });

        return results;
    }

    function addToggleButton(engineNameEl, engineName) {
        // Check if button already exists
        if (engineNameEl.nextElementSibling?.classList?.contains('vt-enhance-quick-toggle')) {
            return;
        }

        // Create toggle button
        const toggleBtn = document.createElement('span');
        toggleBtn.className = 'vt-enhance-quick-toggle';
        toggleBtn.textContent = '★';
        toggleBtn.title = `Toggle highlighting for ${engineName}`;

        // Mark as active if engine is selected
        if (config.selectedEngines.includes(engineName)) {
            toggleBtn.classList.add('active');
        }

        // Add click handler
        toggleBtn.addEventListener('click', function (e) {
            e.stopPropagation();

            const index = config.selectedEngines.indexOf(engineName);
            if (index !== -1) {
                config.selectedEngines.splice(index, 1);
                toggleBtn.classList.remove('active');
            } else {
                config.selectedEngines.push(engineName);
                toggleBtn.classList.add('active');
            }

            saveConfig();
            scheduleHighlighting();
        });

        // Insert after engine name
        engineNameEl.parentNode?.insertBefore(toggleBtn, engineNameEl.nextSibling);
    }

    // Schedule highlighting with debounce
    function scheduleHighlighting() {
        if (pendingHighlightTask) {
            clearTimeout(pendingHighlightTask);
        }

        pendingHighlightTask = setTimeout(() => {
            highlightEngines();
            pendingHighlightTask = null;
        }, 200);
    }

    // Get detections container - optimized to look for the specific path
    function findDetectionsContainer() {
        // Find the vt-ui-detections-list
        const detectionsList = queryShadowDOM('vt-ui-detections-list')[0];
        if (!detectionsList?.shadowRoot) return null;

        // Find vt-ui-expandable inside shadow root
        const expandable = detectionsList.shadowRoot.querySelector('vt-ui-expandable');
        if (!expandable?.shadowRoot) return null;

        // Find content slot
        const contentSlot = expandable.shadowRoot.querySelector('slot[name="content"]');
        if (!contentSlot) return null;

        // Get assigned elements
        const contentElements = contentSlot.assignedElements();
        if (contentElements.length === 0) return null;

        // Find the span and then the detections div
        const contentSpan = contentElements[0];
        return contentSpan.querySelector('#detections');
    }

    function reorderHighlightedEngines() {
        const detectionsDiv = findDetectionsContainer();
        if (!detectionsDiv) {
            console.log('Could not find the detections container');
            return;
        }

        // Get all detection divs
        const detectionDivs = Array.from(detectionsDiv.querySelectorAll('.detection'));
        if (detectionDivs.length < 2) return;

        // Split into highlighted and non-highlighted engines
        const highlightedDivs = [];
        const nonHighlightedDivs = [];

        detectionDivs.forEach(div => {
            const engineName = getEngineNameFromElement(div);
            if (engineName && config.selectedEngines.includes(engineName)) {
                highlightedDivs.push(div);
            } else {
                nonHighlightedDivs.push(div);
            }
        });

        // Sort highlighted divs alphabetically
        highlightedDivs.sort((a, b) => {
            const aName = getEngineNameFromElement(a) || '';
            const bName = getEngineNameFromElement(b) || '';
            return aName.localeCompare(bName);
        });

        // Reorder elements
        const fragment = document.createDocumentFragment();

        // Remove and add highlighted divs
        highlightedDivs.forEach(div => {
            div.parentNode.removeChild(div);
            fragment.appendChild(div);
        });

        // Remove and add non-highlighted divs
        nonHighlightedDivs.forEach(div => {
            div.parentNode.removeChild(div);
            fragment.appendChild(div);
        });

        // Insert back into container
        detectionsDiv.appendChild(fragment);
    }

    // Extract engine name from element
    function getEngineNameFromElement(element) {
        try {
            // Try most common selector first
            const engineNameEl = element.querySelector('.engine-name');
            if (engineNameEl) {
                return engineNameEl.textContent.trim();
            }

            // Try other possible selectors
            const engineEl = element.querySelector('[data-tooltip-text]');
            if (engineEl) {
                return engineEl.textContent.trim();
            }

            if (element.hasAttribute && element.hasAttribute('label')) {
                return element.getAttribute('label');
            }

            const engineSpan = element.querySelector('.engine span');
            if (engineSpan) {
                return engineSpan.textContent.trim();
            }

            // Last attempt for any text content that could be an engine name
            const text = element.textContent.trim();
            if (text && text.length > 0 && text.length < 30 &&
                !text.includes('http') && !text.match(/^\d+$/)) {
                return text;
            }

            return null;
        } catch (e) {
            return null;
        }
    }

    // Main highlight function
    function highlightEngines() {
        // Remove previous highlighting
        document.querySelectorAll('.vt-enhanced-engine').forEach(el => {
            el.classList.remove('vt-enhanced-engine');
        });

        // Find engine name elements
        const engineNameElements = queryShadowDOM('.engine-name');

        // Add toggle buttons and highlight
        engineNameElements.forEach(engineNameEl => {
            try {
                const engineName = engineNameEl.textContent.trim();
                if (!engineName) return;

                addToggleButton(engineNameEl, engineName);

                if (config.selectedEngines.includes(engineName)) {
                    const detectionDiv = engineNameEl.closest('div.detection') ||
                        engineNameEl.closest('vt-ui-expandable') ||
                        engineNameEl.closest('tr') ||
                        engineNameEl.parentElement;

                    if (detectionDiv) {
                        detectionDiv.classList.add('vt-enhanced-engine');
                    }
                }
            } catch (e) { }
        });

        // Also try expandable elements
        queryShadowDOM('vt-ui-expandable').forEach(row => {
            try {
                const label = row.getAttribute('label');
                let engineName = label;

                if (!engineName) {
                    const nameElement = row.querySelector('span:first-child') ||
                        row.querySelector('.engine-name');
                    if (nameElement) {
                        engineName = nameElement.textContent.trim();
                    }
                }

                if (engineName && config.selectedEngines.includes(engineName)) {
                    row.classList.add('vt-enhanced-engine');
                }
            } catch (e) { }
        });

        // Reorder engines if option is enabled
        if (config.reorderEngines) {
            setTimeout(reorderHighlightedEngines, 50);
        }
    }

    // Monitor for page navigation in SPA
    function observePageChanges() {
        // URL change detection
        let lastUrl = location.href;
        new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                if (location.href.match(/\/gui\/file\/[a-f0-9]+(\/?|\/detection)/i)) {
                    setTimeout(scheduleHighlighting, 1000);
                }
            }
        }).observe(document, { subtree: true, childList: true });

        // DOM changes that might contain detection results
        new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList' &&
                    mutation.addedNodes.length > 0) {

                    // Look for nodes that might be detection-related
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE &&
                            (node.tagName === 'VT-UI-DETECTIONS-LIST' ||
                                node.querySelector?.('.engine-name, vt-ui-expandable[label]'))) {
                            scheduleHighlighting();
                            return;
                        }
                    }
                }
            }
        }).observe(document.body, { childList: true, subtree: true });

        // Listen for tab changes
        document.addEventListener('click', event => {
            if (event.target.closest('vt-ui-tab, .nav-link, .tab')) {
                setTimeout(scheduleHighlighting, 500);
            }
        });
    }

    // Inject styles into shadow roots
    function injectShadowStyles() {
        const darkMode = isDarkMode();
        const highlightColor = darkMode ? config.darkModeHighlightColor : config.highlightColor;
        const styleText = `
            .vt-enhanced-engine {
                background-color: ${highlightColor} !important;
                font-weight: bold !important;
                box-shadow: 0 0 4px rgba(0,0,0,0.2) !important;
                border-radius: 4px !important;
            }
            .vt-enhance-quick-toggle {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 18px;
                height: 18px;
                margin-left: 6px;
                border-radius: 3px;
                cursor: pointer;
                font-size: 12px;
                background-color: ${darkMode ? '#333' : '#eee'};
                color: ${darkMode ? '#aaa' : '#666'};
                border: 1px solid ${darkMode ? '#555' : '#ccc'};
                opacity: 0.7;
                transition: all 0.2s ease;
            }
            .vt-enhance-quick-toggle.active {
                background-color: #4CAF50;
                color: white;
                opacity: 1;
            }
            .vt-enhance-quick-toggle:hover {
                opacity: 1;
            }
        `;

        // Find all shadow roots and inject styles
        function injectToShadows(root = document) {
            root.querySelectorAll('*').forEach(el => {
                if (el.shadowRoot && !el.shadowRoot.querySelector('#vt-enhance-style')) {
                    const style = document.createElement('style');
                    style.id = 'vt-enhance-style';
                    style.textContent = styleText;
                    el.shadowRoot.appendChild(style);

                    // Check one level deeper
                    injectToShadows(el.shadowRoot);
                }
            });
        }

        injectToShadows();
    }

    // Initialize the script
    function init() {
        applyBaseStyles();
        createConfigUI();

        // Initial highlighting with progressive attempts
        setTimeout(() => {
            injectShadowStyles();
            scheduleHighlighting();
        }, 1500);

        setTimeout(() => {
            injectShadowStyles();
            scheduleHighlighting();
        }, 3000);

        // Set up observers
        observePageChanges();

        // Listen for theme changes
        if (window.matchMedia) {
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
                applyBaseStyles();
                injectShadowStyles();
                scheduleHighlighting();
            });
        }
    }

    // Start the script
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();