UNPKG

alepm

Version:

Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features

459 lines (380 loc) 13.9 kB
const crypto = require('crypto'); const fs = require('fs-extra'); const path = require('path'); const fetch = require('node-fetch'); class SecurityManager { constructor() { this.vulnerabilityDB = new Map(); this.trustedPublishers = new Set(); this.securityConfig = this.loadSecurityConfig(); } loadSecurityConfig() { return { enableVulnerabilityCheck: true, enableIntegrityCheck: true, enableSignatureVerification: true, allowedHashAlgorithms: ['sha512', 'sha256'], minKeySize: 2048, maxPackageSize: 100 * 1024 * 1024, // 100MB blockedPackages: new Set(), trustedRegistries: ['https://registry.npmjs.org'], requireSignedPackages: false }; } async verifyIntegrity(packageData, expectedIntegrity) { if (!this.securityConfig.enableIntegrityCheck) { return true; } if (!expectedIntegrity) { throw new Error('Package integrity information missing'); } // Parse integrity string (format: algorithm-hash) const integrityMatch = expectedIntegrity.match(/^(sha\d+)-(.+)$/); if (!integrityMatch) { throw new Error('Invalid integrity format'); } const [, algorithm, expectedHash] = integrityMatch; if (!this.securityConfig.allowedHashAlgorithms.includes(algorithm)) { throw new Error(`Unsupported hash algorithm: ${algorithm}`); } // Calculate actual hash const actualHash = crypto.createHash(algorithm) .update(packageData) .digest('base64'); if (actualHash !== expectedHash) { throw new Error('Package integrity verification failed'); } return true; } async verifySignature(packageData, signature, publicKey) { if (!this.securityConfig.enableSignatureVerification) { return true; } if (!signature || !publicKey) { if (this.securityConfig.requireSignedPackages) { throw new Error('Package signature required but not provided'); } return true; } try { const verify = crypto.createVerify('SHA256'); verify.update(packageData); const isValid = verify.verify(publicKey, signature, 'base64'); if (!isValid) { throw new Error('Package signature verification failed'); } return true; } catch (error) { throw new Error(`Signature verification error: ${error.message}`); } } async checkPackageSize(packageData) { if (packageData.length > this.securityConfig.maxPackageSize) { throw new Error(`Package size exceeds maximum allowed (${this.formatBytes(this.securityConfig.maxPackageSize)})`); } return true; } async checkBlockedPackages(packageName) { if (this.securityConfig.blockedPackages.has(packageName)) { throw new Error(`Package "${packageName}" is blocked for security reasons`); } return true; } async scanPackageContent(packageData, packageName) { // Unpack and scan package content for suspicious patterns const suspiciousPatterns = [ /eval\s*\(/gi, // eval calls /Function\s*\(/gi, // Function constructor /require\s*\(\s*['"]child_process['"]/gi, // child_process usage /\.exec\s*\(/gi, // exec calls /\.spawn\s*\(/gi, // spawn calls /fs\.unlink/gi, // file deletion /rm\s+-rf/gi, // dangerous rm commands /curl\s+.*\|\s*sh/gi, // curl pipe to shell /wget\s+.*\|\s*sh/gi, // wget pipe to shell /bitcoin|cryptocurrency|mining|crypto/gi, // crypto mining /password|token|secret|api[_-]?key/gi, // potential credential harvesting ]; const maliciousIndicators = []; try { // Convert buffer to string for pattern matching const content = packageData.toString(); for (const pattern of suspiciousPatterns) { const matches = content.match(pattern); if (matches) { maliciousIndicators.push({ pattern: pattern.source, matches: matches.slice(0, 5), // Limit to first 5 matches severity: this.getPatternSeverity(pattern) }); } } // Check for obfuscated code if (this.detectObfuscation(content)) { maliciousIndicators.push({ pattern: 'Code obfuscation detected', severity: 'medium' }); } // Check for network calls to suspicious domains const suspiciousDomains = this.extractSuspiciousDomains(content); if (suspiciousDomains.length > 0) { maliciousIndicators.push({ pattern: 'Suspicious network activity', domains: suspiciousDomains, severity: 'high' }); } } catch (error) { // If we can't scan the content, log it but don't fail console.warn(`Warning: Could not scan package content for ${packageName}: ${error.message}`); } return maliciousIndicators; } getPatternSeverity(pattern) { const highRisk = [ /eval\s*\(/gi, /Function\s*\(/gi, /\.exec\s*\(/gi, /rm\s+-rf/gi, /curl\s+.*\|\s*sh/gi, /wget\s+.*\|\s*sh/gi ]; return highRisk.some(p => p.source === pattern.source) ? 'high' : 'medium'; } detectObfuscation(content) { // Simple obfuscation detection heuristics const indicators = [ content.includes('\\x'), // Hex encoding content.includes('\\u'), // Unicode encoding content.match(/[a-zA-Z_$][a-zA-Z0-9_$]{20,}/g)?.length > 10, // Long variable names content.includes('unescape'), // URL decoding content.includes('fromCharCode'), // Character code conversion (content.match(/;/g)?.length || 0) > content.split('\n').length * 2 // Too many semicolons ]; return indicators.filter(Boolean).length >= 3; } extractSuspiciousDomains(content) { const urlRegex = /https?:\/\/([\w.-]+)/gi; const urls = content.match(urlRegex) || []; const suspiciousKeywords = [ 'bit.ly', 'tinyurl', 'goo.gl', 'ow.ly', // URL shorteners 'pastebin', 'hastebin', 'ghostbin', // Paste sites 'githubusercontent', // Raw GitHub content 'dropbox', 'mediafire', // File sharing 'onion', '.tk', '.ml' // Suspicious TLDs ]; return urls.filter(url => suspiciousKeywords.some(keyword => url.toLowerCase().includes(keyword)) ); } async auditPackages(packages) { if (!this.securityConfig.enableVulnerabilityCheck) { return []; } const vulnerabilities = []; for (const pkg of packages) { // Check our local vulnerability database const localVulns = await this.checkLocalVulnerabilities(pkg); vulnerabilities.push(...localVulns); // Check online vulnerability databases const onlineVulns = await this.checkOnlineVulnerabilities(pkg); vulnerabilities.push(...onlineVulns); } return vulnerabilities; } async checkLocalVulnerabilities(pkg) { const vulnerabilities = []; const key = `${pkg.name}@${pkg.version}`; if (this.vulnerabilityDB.has(key)) { const vulnData = this.vulnerabilityDB.get(key); vulnerabilities.push({ id: vulnData.id, module_name: pkg.name, version: pkg.version, title: vulnData.title, severity: vulnData.severity, overview: vulnData.overview, recommendation: vulnData.recommendation, fixAvailable: vulnData.fixAvailable, fixVersion: vulnData.fixVersion }); } return vulnerabilities; } async checkOnlineVulnerabilities(pkg) { try { // Check npm audit API (simplified) const response = await fetch('https://registry.npmjs.org/-/npm/v1/security/audits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: pkg.name, version: pkg.version, requires: { [pkg.name]: pkg.version } }), timeout: 5000 }); if (response.ok) { const data = await response.json(); return this.parseVulnerabilityResponse(data, pkg); } } catch (error) { // Fail silently on network errors console.warn(`Could not check vulnerabilities for ${pkg.name}: ${error.message}`); } return []; } parseVulnerabilityResponse(data, pkg) { const vulnerabilities = []; if (data.advisories) { for (const [id, advisory] of Object.entries(data.advisories)) { vulnerabilities.push({ id, module_name: advisory.module_name, version: pkg.version, title: advisory.title, severity: advisory.severity, overview: advisory.overview, recommendation: advisory.recommendation, fixAvailable: !!advisory.patched_versions, fixVersion: advisory.patched_versions }); } } return vulnerabilities; } async validateRegistrySource(registryUrl) { return this.securityConfig.trustedRegistries.includes(registryUrl); } async generatePackageHash(packageData, algorithm = 'sha512') { return crypto.createHash(algorithm) .update(packageData) .digest('base64'); } async createIntegrityString(packageData, algorithm = 'sha512') { const hash = await this.generatePackageHash(packageData, algorithm); return `${algorithm}-${hash}`; } async quarantinePackage(packageName, version, reason) { const quarantineDir = path.join(require('os').homedir(), '.alepm', 'quarantine'); await fs.ensureDir(quarantineDir); const quarantineData = { packageName, version, reason, timestamp: Date.now(), id: crypto.randomUUID() }; const quarantineFile = path.join(quarantineDir, `${packageName}-${version}-${Date.now()}.json`); await fs.writeJson(quarantineFile, quarantineData, { spaces: 2 }); // Add to blocked packages this.securityConfig.blockedPackages.add(packageName); console.warn(`Package ${packageName}@${version} has been quarantined: ${reason}`); } async updateVulnerabilityDatabase() { try { // Download latest vulnerability database const response = await fetch('https://raw.githubusercontent.com/nodejs/security-wg/main/vuln/npm/advisories.json', { timeout: 10000 }); if (response.ok) { const vulnerabilities = await response.json(); // Update local database for (const vuln of vulnerabilities) { const key = `${vuln.module_name}@${vuln.version}`; this.vulnerabilityDB.set(key, vuln); } // Save to disk const dbPath = path.join(require('os').homedir(), '.alepm', 'vulnerability-db.json'); await fs.writeJson(dbPath, Object.fromEntries(this.vulnerabilityDB), { spaces: 2 }); return vulnerabilities.length; } } catch (error) { console.warn(`Could not update vulnerability database: ${error.message}`); } return 0; } async audit(packages) { const results = []; for (const pkg of packages) { // Basic security checks await this.checkBlockedPackages(pkg.name); // Vulnerability check const vulnerabilities = await this.auditPackages([pkg]); results.push(...vulnerabilities); } return results; } formatBytes(bytes) { const sizes = ['B', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 B'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } // Security policy management async addTrustedPublisher(publisherId, publicKey) { this.trustedPublishers.add({ id: publisherId, publicKey, addedAt: Date.now() }); } async removeTrustedPublisher(publisherId) { this.trustedPublishers.delete(publisherId); } async blockPackage(packageName, reason) { this.securityConfig.blockedPackages.add(packageName); await this.quarantinePackage(packageName, '*', reason); } async unblockPackage(packageName) { this.securityConfig.blockedPackages.delete(packageName); } // Risk assessment assessPackageRisk(pkg, scanResults) { let riskScore = 0; const factors = []; // Age factor (newer packages are riskier) const packageAge = Date.now() - new Date(pkg.time?.created || 0).getTime(); const ageInDays = packageAge / (1000 * 60 * 60 * 24); if (ageInDays < 30) { riskScore += 2; factors.push('Package is very new (< 30 days)'); } else if (ageInDays < 90) { riskScore += 1; factors.push('Package is relatively new (< 90 days)'); } // Download count factor if (pkg.downloads?.weekly < 100) { riskScore += 2; factors.push('Low download count'); } // Maintainer factor if (!pkg.maintainers || pkg.maintainers.length === 0) { riskScore += 1; factors.push('No maintainers'); } // Scan results factor if (scanResults.length > 0) { const highSeverity = scanResults.filter(r => r.severity === 'high').length; const mediumSeverity = scanResults.filter(r => r.severity === 'medium').length; riskScore += highSeverity * 3 + mediumSeverity * 1; factors.push(`${scanResults.length} suspicious patterns detected`); } // Dependencies factor if (pkg.dependencies && Object.keys(pkg.dependencies).length > 50) { riskScore += 1; factors.push('Many dependencies (> 50)'); } return { score: riskScore, level: riskScore === 0 ? 'low' : riskScore <= 3 ? 'medium' : 'high', factors }; } } module.exports = SecurityManager;