@mathiscode/codebase-scanner
Version:
Scan a codebase for malware signatures
482 lines (405 loc) • 19.9 kB
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>