Complete transparency about our client-side vulnerability scanner. For security-conscious developers who need to know exactly how their tools work.
A deep dive into how we built a completely client-side vulnerability scanner
All lockfile parsing happens in your browser using pure JavaScript. No server uploads required. We support npm, pnpm, and Yarn lockfiles with format detection.
// Parse npm lockfile locally
extractNpmDependencies(lockData, dependencies) {
if (lockData.packages) {
// Handle v2/v3 format
Object.entries(lockData.packages).forEach(([path, pkg]) => {
if (path && pkg.version) {
const name = path.replace('node_modules/', '');
dependencies.set(name, pkg.version);
}
});
} else if (lockData.dependencies) {
// Handle v1 format
this.extractNpmV1Dependencies(lockData.dependencies, dependencies);
}
}
// File reading with FileReader API
readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsText(file); // Never uploaded to server
});
}
Dependencies are matched against our pre-loaded vulnerability database using efficient algorithms. Critical incidents are checked first, followed by known vulnerabilities.
// Check vulnerabilities locally
async checkVulnerabilities(dependencies) {
const results = [];
for (const dep of dependencies) {
const result = {
name: dep.name,
version: dep.version,
status: 'ok',
advisories: []
};
// Check incidents first (critical supply chain attacks)
if (this.incidentData[dep.name]) {
const incident = this.incidentData[dep.name];
if (this.versionMatches(dep.version, incident.versions)) {
result.status = 'critical';
result.advisories.push({
id: incident.advisory,
severity: incident.severity,
summary: incident.summary
});
}
}
// Check vulnerability database
if (this.vulnerabilityIndex[dep.name] && result.status !== 'critical') {
const vulnRecord = this.vulnerabilityIndex[dep.name];
const ranges = Array.isArray(vulnRecord.ranges) ? vulnRecord.ranges : [vulnRecord.ranges];
const isAffected = ranges.some(range => this.versionMatches(dep.version, range));
if (isAffected) {
result.status = 'vulnerable';
result.advisories.push(...vulnRecord.ids.map(id => ({
id: id,
severity: vulnRecord.severity || 'MODERATE',
summary: `Vulnerability in ${dep.name}`
})));
}
}
results.push(result);
}
return results.sort((a, b) => {
const statusOrder = { critical: 0, vulnerable: 1, ok: 2 };
return statusOrder[a.status] - statusOrder[b.status];
});
}
We pre-process Google's Open Source Vulnerabilities database into an optimized format for fast client-side lookups. Fallback to live OSV API for packages not in our index.
// Load vulnerability data
async loadVulnerabilityData() {
try {
const [vulnData, incidentData] = await Promise.all([
fetch('./data/npm-index.json').then(r => r.json()),
this.loadIncidentOverlay()
]);
this.vulnerabilityIndex = vulnData.vulnerabilities || {};
this.incidentData = incidentData;
console.log(`Loaded ${Object.keys(this.vulnerabilityIndex).length} packages with vulnerabilities`);
console.log(`Loaded ${Object.keys(this.incidentData).length} security incidents`);
} catch (error) {
console.warn('Failed to load pre-built data, using OSV API fallback');
this.vulnerabilityIndex = {};
this.incidentData = {};
}
}
// OSV API fallback for unknown packages
async queryOSVAPI(packageName, version) {
const query = {
package: { name: packageName, ecosystem: "npm" },
version: version
};
const response = await fetch('https://api.osv.dev/v1/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(query)
});
const data = await response.json();
return (data.vulns || []).map(vuln => ({
id: vuln.id,
severity: this.mapOSVSeverity(vuln.severity),
summary: vuln.summary || `Vulnerability in ${packageName}`
}));
}
File processing uses the FileReader API to keep everything local to your browser. Drag & drop is handled entirely client-side with no network requests for file content.
// Drag & drop handling - completely local
setupEventListeners() {
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0) {
this.handleFile(files[0]); // Process locally only
}
});
// Also handle document-wide drops
document.addEventListener('drop', (e) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
this.handleFile(files[0]);
}
});
}
// File validation and processing
async handleFile(file) {
const fileName = file.name.toLowerCase();
// Validate file type locally
if (!this.isValidLockfile(fileName)) {
this.showError('Please upload a valid lockfile');
return;
}
this.showLoading(true);
try {
const content = await this.readFile(file); // Local only
const dependencies = await this.parseLockfile(content, fileName);
const results = await this.checkVulnerabilities(dependencies);
this.displayResults(results);
} catch (error) {
this.showError('Failed to process lockfile: ' + error.message);
} finally {
this.showLoading(false);
}
}
We use localStorage to cache OSV API responses and implement intelligent batching to avoid overwhelming external services while maintaining fast response times.
// Smart caching system
getOSVCache() {
try {
const cache = localStorage.getItem('osv-cache');
return cache ? JSON.parse(cache) : {};
} catch (error) {
console.warn('Failed to load OSV cache:', error);
return {};
}
}
saveOSVCache(cache) {
try {
// Keep cache size reasonable - remove entries older than 7 days
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
const cleanedCache = {};
Object.entries(cache).forEach(([key, value]) => {
if (value.timestamp && value.timestamp > oneWeekAgo) {
cleanedCache[key] = value;
}
});
localStorage.setItem('osv-cache', JSON.stringify(cleanedCache));
} catch (error) {
console.warn('Failed to save OSV cache:', error);
}
}
// Batched API requests
async checkPackagesViaOSV(packagesNeedingCheck, osvCache) {
const batchSize = 10; // Process in batches
for (let i = 0; i < packagesNeedingCheck.length; i += batchSize) {
const batch = packagesNeedingCheck.slice(i, i + batchSize);
await Promise.all(batch.map(async ({ dep, result }) => {
try {
const vulnerabilities = await this.queryOSVAPI(dep.name, dep.version);
const cacheKey = `${dep.name}@${dep.version}`;
// Cache the result
osvCache[cacheKey] = {
vulnerabilities,
timestamp: Date.now()
};
if (vulnerabilities.length > 0) {
result.status = 'vulnerable';
result.advisories.push(...vulnerabilities);
}
} catch (error) {
console.warn(`Failed to check ${dep.name}@${dep.version}:`, error);
}
}));
// Small delay between batches to be respectful
if (i + batchSize < packagesNeedingCheck.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
this.saveOSVCache(osvCache);
}
Sophisticated version range matching that handles semantic versioning, wildcards, and complex range expressions commonly found in vulnerability databases.
// Version matching with semver support
versionMatches(version, range) {
if (range === '*') return true;
// Handle common range patterns
if (range.startsWith('<')) {
const targetVersion = range.substring(1);
return this.compareVersions(version, targetVersion) < 0;
}
if (range.startsWith('>=')) {
const targetVersion = range.substring(2);
return this.compareVersions(version, targetVersion) >= 0;
}
if (range.startsWith('>')) {
const targetVersion = range.substring(1);
return this.compareVersions(version, targetVersion) > 0;
}
if (range.startsWith('<=')) {
const targetVersion = range.substring(2);
return this.compareVersions(version, targetVersion) <= 0;
}
// Exact match
return version === range;
}
// Semantic version comparison
compareVersions(a, b) {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = aParts[i] || 0;
const bPart = bParts[i] || 0;
if (aPart < bPart) return -1;
if (aPart > bPart) return 1;
}
return 0;
}
While we don't publish our full source code, we provide complete transparency about our methods. Every algorithm and approach is documented here with real code examples.
Your files never leave your browser. We use standard web APIs (FileReader, fetch) with no custom network protocols or hidden data transmission.
All vulnerability data comes from Google's OSV database and official security advisories. We don't create or modify vulnerability information.
We don't track what packages you scan, what vulnerabilities you find, or any other usage data. Analytics are limited to basic page views.