您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();