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
JavaScript
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;