UNPKG

@mathiscode/codebase-scanner

Version:
482 lines (405 loc) 19.9 kB
<!DOCTYPE html> <html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Codebase Scanner - NPM Detections</title> <link rel='stylesheet' href='style.css'> <style> /* Optional: Add a loading indicator style */ .loading-indicator { text-align: center; padding: 20px; font-style: italic; color: #666; display: none; /* Hidden by default */ } .search-note { font-size: 0.9em; color: #555; margin-bottom: 15px; text-align: center; } .malicious-card { background-color: rgba(255, 0, 0, 0.1); /* Light red tint */ border-left: 5px solid red; } .warning-card { background-color: rgba(255, 255, 0, 0.1); /* Light yellow tint */ border-left: 5px solid orange; } </style> </head> <body> <div class='header'> <h1>NPM Registry Malware Scan Results</h1> <h2>Scanned with <a href='https://github.com/mathiscode/codebase-scanner#readme' target='_blank'>codebase-scanner</a> by <a href='https://github.com/mathiscode' target='_blank'>Jay Mathis</a></h2> <p id='package-count' style='font-size: 1.2em; text-align: center; border: 1px solid #ccc; padding: 10px; border-radius: 5px; background-color: #f9f9f9;'></p> <div style='display: flex; align-items: center; justify-content: center; gap: 10px; font-size: 0.8em; color: #777;'> <progress id='scan-progress' value='0' max='100'></progress> <span id='scan-progress-text' /> </div> <p style='text-align: center; margin-bottom: 20px;'> Groups scanned as of <span class='today-date'></span>: <br /> <span id='available-chunks' style='font-weight: bold'></span> </p> <p> I'm donating the resources to scan the entire NPM registry, but please consider sponsoring to help out! <br /> <a href="https://github.com/sponsors/mathiscode" target="_blank" rel="noopener noreferrer"> <img alt="Sponsor mathiscode on GitHub" src="https://img.shields.io/github/sponsors/mathiscode?color=red&label=sponsors" /> </a> </p> <hr /> <p>Please note that many legitimate packages may use techniques that trigger false positives.</p> <p>We want to draw attention to these packages so that they can be investigated.</p> <p>A whitelist/false positive system is coming soon.</p> <p>If a package is not listed here, it either has not been scanned yet or did not trigger any detections.</p> <hr /> <p style='display: none;'> Loaded Chunks: <span id='loaded-chunks'></span> </p> </div> <div id='search-container'> <input type='text' id='search-input' placeholder='Search packages...'> </div> <div id='results-container'> <p>Loading data...</p> </div> <div id='loading-indicator' class='loading-indicator'>Loading more packages...</div> <script> let allLoadedPackages = [] // Holds packages loaded from all chunks so far let chunkFiles = [] // List of all available chunk filenames (e.g., ['a.json', 'b.json']) let currentChunkIndex = 0 // Index of the next chunk to load for scrolling let isLoading = false // Flag to prevent multiple simultaneous loads (for scrolling or search) let allChunksLoaded = false // Flag to indicate if all chunks have been loaded via scrolling let loadedChunkFiles = new Set() // Tracks filenames of chunks already loaded let signatures = [] // Global signatures array const resultsContainer = document.getElementById('results-container') const searchInput = document.getElementById('search-input') const loadingIndicator = document.getElementById('loading-indicator') const loadedChunksSpan = document.getElementById('loaded-chunks') const availableChunksSpan = document.getElementById('available-chunks') const todayDateSpan = document.querySelectorAll('.today-date') const dataBasePath = 'data/npm/' for (const span of todayDateSpan) span.textContent = new Date().toLocaleDateString() function updateLoadedChunksDisplay() { const loadedFilesArray = Array.from(loadedChunkFiles).sort() loadedChunksSpan.textContent = loadedFilesArray.length > 0 ? loadedFilesArray.join(', ') : 'None' } function updateAvailableChunksDisplay() { let groups = chunkFiles.map(chunk => chunk.split('.')[0]).join(', ').toUpperCase() const lastGroup = groups.split(', ').pop() const lastGroupSpan = document.createElement('span') lastGroupSpan.textContent = lastGroup lastGroupSpan.style.color = 'orange' lastGroupSpan.style.cursor = 'progress' lastGroupSpan.setAttribute('title', 'Partially Scanned') groups = groups.replace(lastGroup, lastGroupSpan.outerHTML) availableChunksSpan.innerHTML = groups } function createPackageCard(pkg) { const card = document.createElement('div') card.className = 'package-card' const scannedDate = new Date(pkg.scanned_at * 1000).toLocaleString() const findingsArray = pkg.findings const findingsExist = Array.isArray(findingsArray) && findingsArray.length > 0 let isMalicious = false let hasOnlyWarnings = false if (findingsExist) { let warningCount = 0 for (const findingDetail of findingsArray) { const signature = signatures[findingDetail.index] if (signature && signature.level) { const level = signature.level.toLowerCase() if (level === 'malicious') { isMalicious = true break } else if (level === 'warning') { warningCount++ } } } if (!isMalicious && warningCount > 0 && warningCount === findingsArray.length) { hasOnlyWarnings = true } } if (isMalicious) card.classList.add('malicious-card') else if (hasOnlyWarnings) card.classList.add('warning-card') card.innerHTML = ` <div class='card-content'> <h2><a href='https://www.npmjs.com/package/${pkg.name}' target='_blank' rel='noopener noreferrer'>${pkg.name}</a></h2> <p>Detections: <strong>${pkg.detections_count}</strong></p> <div class='details'> <span>Files Scanned: ${pkg.files_scanned ?? 'N/A'}</span><br> <span>Scanned At: ${scannedDate}</span> </div> </div> <div class='findings-control'> ${findingsExist ? '<span class="findings-toggle">Show Findings</span>' : '<span class="no-findings-info">No specific findings data available.</span>'} ${findingsExist ? '<div class="findings-details" style="display: none;"></div>' : ''} </div> ` if (findingsExist) { const toggle = card.querySelector('.findings-toggle') const detailsDiv = card.querySelector('.findings-details') toggle.onclick = () => { const currentlyVisible = detailsDiv.style.display !== 'none' if (!currentlyVisible && detailsDiv.innerHTML === '') { try { if (findingsArray.length > 0) { const table = document.createElement('table') const thead = document.createElement('thead') const tbody = document.createElement('tbody') thead.innerHTML = ` <tr> <th>Rule Name</th> <th>Level</th> <th>File</th> </tr> ` findingsArray.forEach(findingDetail => { const signature = signatures[findingDetail.index] const tr = document.createElement('tr') if (signature) { tr.innerHTML = ` <td>${signature.name || 'N/A'}</td> <td>${signature.level || 'N/A'}</td> <td>${findingDetail.file || 'N/A'}</td> ` } else { tr.innerHTML = `<td colspan='3' style='color: orange;'>Signature details not found for index: ${findingDetail.index}</td>` console.warn(`Signature not found for index: ${findingDetail.index} in package ${pkg.name}`) } tbody.appendChild(tr) }) table.appendChild(thead) table.appendChild(tbody) detailsDiv.appendChild(table) } else { detailsDiv.innerHTML = '<p>No triggered detections found in scan data.</p>' } } catch (e) { console.error('Error processing findings for package:', pkg.name, e) detailsDiv.innerHTML = '<p style="color: red;">Could not display findings (error during processing).</p>' } } detailsDiv.style.display = currentlyVisible ? 'none' : 'block' toggle.textContent = currentlyVisible ? 'Show Findings' : 'Hide Findings' } } return card } function getChunkFilenameForTerm(term) { if (!term) return null const firstChar = term.toLowerCase()[0] const digits = '0123456789' const alphabet = 'abcdefghijklmnopqrstuvwxyz' let baseFilename = '' if (alphabet.includes(firstChar)) baseFilename = `${firstChar}.json` else if (digits.includes(firstChar)) baseFilename = `${firstChar}.json` else baseFilename = 'other.json' return `${baseFilename}.gz` } function renderPackages(packagesToRender) { resultsContainer.innerHTML = '' const searchTermActive = searchInput.value.trim() !== '' if (packagesToRender.length === 0) { if (searchTermActive) resultsContainer.innerHTML = '<p class="no-results">No matching packages found in the currently loaded data.</p>' else resultsContainer.innerHTML = '<p>Type to search or scroll down to load packages.</p>' return } const groups = {} const digits = '0123456789' const alphabet = 'abcdefghijklmnopqrstuvwxyz' packagesToRender.forEach(pkg => { if (!pkg || !pkg.name) return const firstChar = pkg.name.toLowerCase()[0] let groupKey = '' if (alphabet.includes(firstChar)) groupKey = firstChar.toUpperCase() else if (digits.includes(firstChar)) groupKey = firstChar else groupKey = 'Other' if (!groups[groupKey]) groups[groupKey] = [] groups[groupKey].push(pkg) }) const groupOrder = [...digits.split(''), ...Array.from(alphabet.toUpperCase()), 'Other'] const sortedGroupKeys = Object.keys(groups).sort((a, b) => { const indexA = groupOrder.indexOf(a) const indexB = groupOrder.indexOf(b) if (indexA === -1 && indexB === -1) return a.localeCompare(b) if (indexA === -1) return 1 if (indexB === -1) return -1 return indexA - indexB }) sortedGroupKeys.forEach(key => { const heading = document.createElement('h3') heading.textContent = key heading.className = 'group-heading' resultsContainer.appendChild(heading) groups[key].forEach(pkg => { const card = createPackageCard(pkg) resultsContainer.appendChild(card) }) }) } async function filterAndRenderPackages() { if (isLoading) return const searchTerm = searchInput.value.toLowerCase().trim() if (!searchTerm) return renderPackages(allLoadedPackages) const targetChunkFile = getChunkFilenameForTerm(searchTerm) let newChunkLoaded = false if (targetChunkFile && chunkFiles.includes(targetChunkFile) && !loadedChunkFiles.has(targetChunkFile)) { isLoading = true loadingIndicator.textContent = `Loading data for '${targetChunkFile.split('.')[0]}...'` loadingIndicator.style.display = 'block' resultsContainer.innerHTML = '' const newPackages = await fetchChunk(targetChunkFile) if (newPackages.length > 0) { const currentPackageNames = new Set(allLoadedPackages.map(p => p.name)) const uniqueNewPackages = newPackages.filter(p => !currentPackageNames.has(p.name)) allLoadedPackages.push(...uniqueNewPackages) allLoadedPackages.sort((a, b) => a.name.localeCompare(b.name)) } loadedChunkFiles.add(targetChunkFile) updateLoadedChunksDisplay() loadingIndicator.style.display = 'none' isLoading = false newChunkLoaded = true } const filteredPackages = allLoadedPackages.filter(pkg => pkg.name.toLowerCase().includes(searchTerm)) renderPackages(filteredPackages) if (newChunkLoaded && filteredPackages.length > 0 && document.activeElement !== searchInput) { const firstChar = searchTerm.toLowerCase()[0] const digits = '0123456789' const alphabet = 'abcdefghijklmnopqrstuvwxyz' let groupKeyToScrollTo = '' if (alphabet.includes(firstChar)) { groupKeyToScrollTo = firstChar.toUpperCase() } else if (digits.includes(firstChar)) { groupKeyToScrollTo = firstChar } else { groupKeyToScrollTo = 'Other' } const headings = resultsContainer.querySelectorAll('.group-heading') for (const heading of headings) { if (heading.textContent === groupKeyToScrollTo) { heading.scrollIntoView({ behavior: 'smooth', block: 'start' }) break } } } } searchInput.addEventListener('input', filterAndRenderPackages) async function fetchChunk(chunkFilename) { if (!chunkFilename) return [] try { const response = await fetch(dataBasePath + chunkFilename) if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${chunkFilename}`) if (typeof DecompressionStream === 'undefined') { console.warn('DecompressionStream API not supported in this browser. Cannot process gzipped files.') throw new Error(`DecompressionStream not supported. Failed to load ${chunkFilename}.`) } if (!response.body) throw new Error(`Response body is null for ${chunkFilename}.`) const decompressionStream = new DecompressionStream('gzip') const decompressedStream = response.body.pipeThrough(decompressionStream) const decompressedText = await new Response(decompressedStream).text() const chunkData = JSON.parse(decompressedText) return Array.isArray(chunkData) ? chunkData : [] } catch (error) { console.error(`Error in fetchChunk for ${chunkFilename}:`, error) if (loadingIndicator) { loadingIndicator.textContent = `Error loading ${chunkFilename}. Check console.` loadingIndicator.style.display = 'block' } return [] } } async function loadNextChunk() { if (isLoading || allChunksLoaded || currentChunkIndex >= chunkFiles.length) return isLoading = true loadingIndicator.style.display = 'block' loadingIndicator.textContent = 'Loading more packages...' const nextChunkFile = chunkFiles[currentChunkIndex] let newPackages = [] if (loadedChunkFiles.has(nextChunkFile)) { console.log(`Chunk ${nextChunkFile} already loaded, skipping fetch in loadNextChunk.`) } else { newPackages = await fetchChunk(nextChunkFile) if (newPackages.length > 0) { const currentPackageNames = new Set(allLoadedPackages.map(p => p.name)) const uniqueNewPackages = newPackages.filter(p => !currentPackageNames.has(p.name)) allLoadedPackages.push(...uniqueNewPackages) allLoadedPackages.sort((a, b) => a.name.localeCompare(b.name)) } loadedChunkFiles.add(nextChunkFile) } updateLoadedChunksDisplay() if (searchInput.value.trim() === '') renderPackages(allLoadedPackages) else filterAndRenderPackages() currentChunkIndex++ if (currentChunkIndex >= chunkFiles.length) { allChunksLoaded = true loadingIndicator.textContent = 'All packages loaded.' } else { loadingIndicator.style.display = 'none' } isLoading = false } async function initialize() { try { const response = await fetch(dataBasePath + 'index.json') if (!response.ok) throw new Error(`HTTP error! status: ${response.status} fetching index.json`) chunkFiles = await response.json() if (!Array.isArray(chunkFiles) || chunkFiles.length === 0) { resultsContainer.innerHTML = '<p class="no-results">No data chunks found. Run the generation script.</p>' loadingIndicator.style.display = 'none' return } const countResponse = await fetch(dataBasePath + 'count.json') if (!countResponse.ok) throw new Error(`HTTP error! status: ${countResponse.status} fetching count.json`) const { count, total, npmPackageCount } = await countResponse.json() document.getElementById('package-count').innerHTML = ` <strong>${new Intl.NumberFormat().format(count)}</strong> flagged packages out of <strong>${new Intl.NumberFormat().format(total)}</strong> total packages scanned so far of the ~<strong>${new Intl.NumberFormat().format(npmPackageCount)}</strong> packages in the <a href='https://npmjs.com' target='_blank' rel='noopener noreferrer'>NPM registry</a>. ` const signaturesResponse = await fetch(dataBasePath + 'signatures.json') if (!signaturesResponse.ok) throw new Error(`HTTP error! status: ${signaturesResponse.status} fetching signatures.json`) signatures = await signaturesResponse.json() const progress = document.getElementById('scan-progress') progress.value = total progress.max = npmPackageCount const percentage = ((total / npmPackageCount) * 100).toFixed(2) document.getElementById('scan-progress-text').textContent = `${percentage}% (${new Intl.NumberFormat().format(total)} / ${new Intl.NumberFormat().format(npmPackageCount)})` await loadNextChunk() if(allLoadedPackages.length === 0 && !allChunksLoaded) await loadNextChunk() if(allLoadedPackages.length === 0 && allChunksLoaded) { resultsContainer.innerHTML = '<p class="no-results">No packages with detections found in any data file.</p>' loadingIndicator.style.display = 'none' } updateLoadedChunksDisplay() updateAvailableChunksDisplay() } catch (error) { console.error('Error initializing page:', error) resultsContainer.innerHTML = '<p class="no-results">Error loading initial data index. Please check console.</p>' loadingIndicator.style.display = 'none' } } window.addEventListener('scroll', () => { // Only trigger scroll load if search is not active OR if search is active but user scrolled to bottom anyway const isSearchActive = searchInput.value.trim() !== '' const scrollThreshold = document.documentElement.scrollHeight - 300 // px from bottom if (!isLoading && !allChunksLoaded && (window.innerHeight + window.scrollY) >= scrollThreshold) { loadNextChunk() } }) initialize() </script> <script src="https://cdn.counter.dev/script.js" data-id="50edfcc8-6275-4a2d-9be3-d5200aa67416" data-utcoffset="-6"></script> </body> </html>