UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

364 lines 16.8 kB
/** * Dependency Scanner * Checks for vulnerable dependencies and runs npm audit */ import fs from 'fs-extra'; import * as path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export class DependencyScanner { projectRoot; constructor(projectRoot) { this.projectRoot = projectRoot; } async scan(vulnerabilities) { await this.checkDependencyVulnerabilities(vulnerabilities); await this.checkPackageJsonSecurity(vulnerabilities); } async runNpmAudit(vulnerabilities) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); // Only run if package.json exists if (!(await fs.pathExists(packageJsonPath))) { return undefined; } try { // Run npm audit with JSON output const { stdout } = await execAsync('npm audit --json', { cwd: this.projectRoot, timeout: 30000 // 30 second timeout }); const auditResult = JSON.parse(stdout); // Process vulnerabilities const npmVulnerabilities = []; if (auditResult.vulnerabilities) { for (const [packageName, vuln] of Object.entries(auditResult.vulnerabilities)) { const vulnerability = vuln; npmVulnerabilities.push({ name: packageName, severity: vulnerability.severity || 'unknown', isDirect: vulnerability.isDirect || false, via: vulnerability.via || [], effects: vulnerability.effects || [], range: vulnerability.range || '', nodes: vulnerability.nodes || [], fixAvailable: vulnerability.fixAvailable || false }); // Add to main vulnerabilities list vulnerabilities.push({ file: 'package.json', severity: this.mapNpmSeverity(vulnerability.severity), type: 'npm_vulnerability', message: `${packageName}: ${vulnerability.title || 'Security vulnerability detected'}`, packageName: packageName, packageVersion: vulnerability.range, cve: vulnerability.cves?.join(', '), recommendation: vulnerability.fixAvailable ? 'Update available' : 'No automated fix available' }); } } const summary = auditResult.metadata?.vulnerabilities || {}; return { totalVulnerabilities: summary.total || 0, critical: summary.critical || 0, high: summary.high || 0, moderate: summary.moderate || 0, low: summary.low || 0, info: summary.info || 0, auditReportMajorVersion: auditResult.auditReportVersion || 2, vulnerabilities: npmVulnerabilities }; } catch (error) { // NPM audit failed (could be no vulnerabilities or npm not available) // This is not necessarily an error condition return undefined; } } async checkDependencyVulnerabilities(vulnerabilities) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (await fs.pathExists(packageJsonPath)) { try { const packageJson = await fs.readJson(packageJsonPath); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Check for known vulnerable packages and patterns await this.checkKnownVulnerablePackages(allDeps, vulnerabilities); await this.checkOutdatedPackages(allDeps, vulnerabilities); await this.checkSuspiciousPackages(allDeps, vulnerabilities); } catch (error) { // Ignore JSON parsing errors } } } async checkPackageJsonSecurity(vulnerabilities) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (await fs.pathExists(packageJsonPath)) { try { const packageJson = await fs.readJson(packageJsonPath); // Check for dangerous scripts if (packageJson.scripts) { for (const [scriptName, scriptValue] of Object.entries(packageJson.scripts)) { if (typeof scriptValue === 'string') { this.checkDangerousScript(scriptName, scriptValue, vulnerabilities); } } } // Check for missing security-related scripts this.checkSecurityScripts(packageJson.scripts || {}, vulnerabilities); // Check for postinstall scripts (can be security risk) if (packageJson.scripts?.postinstall) { vulnerabilities.push({ file: 'package.json', severity: 'medium', type: 'postinstall_script', message: 'postinstall script detected - ensure it\'s from a trusted source', recommendation: 'Review postinstall script for malicious behavior' }); } } catch (error) { // Ignore JSON parsing errors } } } async checkKnownVulnerablePackages(deps, vulnerabilities) { // Known vulnerable packages with specific versions const knownVulnerable = [ { package: 'event-stream', version: '3.3.6', cve: 'CVE-2018-16487' }, { package: 'lodash', maxVersion: '4.17.20', cve: 'CVE-2021-23337' }, { package: 'axios', maxVersion: '0.21.0', cve: 'CVE-2020-28168' }, { package: 'minimist', maxVersion: '1.2.5', cve: 'CVE-2021-44906' }, { package: 'node-fetch', maxVersion: '2.6.6', cve: 'CVE-2022-0235' }, { package: 'tar', maxVersion: '6.1.11', cve: 'CVE-2021-37701' }, { package: 'glob-parent', maxVersion: '5.1.1', cve: 'CVE-2020-28469' }, { package: 'express-fileupload', maxVersion: '1.1.8', cve: 'CVE-2020-7699' } ]; for (const [depName, depVersion] of Object.entries(deps)) { // Check against known vulnerable packages for (const vuln of knownVulnerable) { if (depName === vuln.package) { const version = this.extractVersion(depVersion); if (vuln.version && version === vuln.version) { vulnerabilities.push({ file: 'package.json', severity: 'critical', type: 'known_vulnerability', message: `${depName}@${version} has known vulnerability`, cve: vuln.cve, packageName: depName, packageVersion: version, recommendation: `Update ${depName} to latest version` }); } else if (vuln.maxVersion && this.isVersionLessThan(version, vuln.maxVersion)) { vulnerabilities.push({ file: 'package.json', severity: 'high', type: 'known_vulnerability', message: `${depName}@${version} may have vulnerability (below ${vuln.maxVersion})`, cve: vuln.cve, packageName: depName, packageVersion: version, recommendation: `Update ${depName} to version > ${vuln.maxVersion}` }); } } } } } async checkOutdatedPackages(deps, vulnerabilities) { for (const [dep, version] of Object.entries(deps)) { if (typeof version === 'string') { // Check for very old versions if (version.startsWith('^0.') || version.startsWith('~0.')) { vulnerabilities.push({ file: 'package.json', severity: 'medium', type: 'outdated_dependency', message: `Dependency ${dep}@${version} appears to be very old (pre-1.0)`, packageName: dep, packageVersion: version, recommendation: 'Consider updating to a stable version' }); } // Check for git dependencies (security risk) if (version.includes('git://') || version.includes('git+')) { vulnerabilities.push({ file: 'package.json', severity: 'high', type: 'git_dependency', message: `Dependency ${dep} uses git URL which may be insecure`, packageName: dep, packageVersion: version, recommendation: 'Use npm registry versions instead of git URLs' }); } // Check for file dependencies (could be malicious) if (version.startsWith('file:')) { vulnerabilities.push({ file: 'package.json', severity: 'medium', type: 'local_dependency', message: `Dependency ${dep} uses local file path`, packageName: dep, packageVersion: version, recommendation: 'Ensure local dependency is from trusted source' }); } } } } async checkSuspiciousPackages(deps, vulnerabilities) { // Patterns that might indicate typosquatting or malicious packages const suspiciousPatterns = [ { pattern: /^node_modules$/, message: 'Package name "node_modules" is suspicious' }, { pattern: /^\.\./, message: 'Package name starting with ".." is suspicious' }, { pattern: /\s/, message: 'Package name contains whitespace' }, { pattern: /[<>|&;`]/, message: 'Package name contains shell metacharacters' } ]; // Common typosquatting targets const commonTypos = { 'reacts': 'react', 'rect': 'react', 'expres': 'express', 'expresss': 'express', 'loadash': 'lodash', 'lodsh': 'lodash', 'momnet': 'moment', 'axois': 'axios', 'axious': 'axios' }; for (const depName of Object.keys(deps)) { // Check suspicious patterns for (const { pattern, message } of suspiciousPatterns) { if (pattern.test(depName)) { vulnerabilities.push({ file: 'package.json', severity: 'high', type: 'suspicious_package', message: `${message}: ${depName}`, packageName: depName, recommendation: 'Verify this package is legitimate' }); } } // Check for typosquatting const lowerDepName = depName.toLowerCase(); if (commonTypos[lowerDepName]) { vulnerabilities.push({ file: 'package.json', severity: 'critical', type: 'typosquatting', message: `Possible typosquatting: "${depName}" (did you mean "${commonTypos[lowerDepName]}"?)`, packageName: depName, recommendation: `Replace with legitimate package: ${commonTypos[lowerDepName]}` }); } } } checkDangerousScript(scriptName, scriptValue, vulnerabilities) { // Check for dangerous commands const dangerousCommands = [ { pattern: /rm\s+-rf\s+\//, severity: 'critical', message: 'Dangerous rm -rf / command' }, { pattern: /rm\s+-rf\s+~/, severity: 'high', message: 'Dangerous rm -rf ~ command' }, { pattern: /curl.*\|\s*sh/, severity: 'critical', message: 'Piping curl to shell is dangerous' }, { pattern: /wget.*\|\s*sh/, severity: 'critical', message: 'Piping wget to shell is dangerous' }, { pattern: /eval\s*\(/, severity: 'high', message: 'eval() usage in script' }, { pattern: /sudo\s+/, severity: 'medium', message: 'Script requires sudo privileges' } ]; for (const { pattern, severity, message } of dangerousCommands) { if (pattern.test(scriptValue)) { vulnerabilities.push({ file: 'package.json', severity, type: 'dangerous_script', message: `Script "${scriptName}": ${message}`, recommendation: 'Review and modify script to remove dangerous commands' }); } } // Check for obfuscated scripts if (this.isObfuscated(scriptValue)) { vulnerabilities.push({ file: 'package.json', severity: 'high', type: 'obfuscated_script', message: `Script "${scriptName}" appears to be obfuscated`, recommendation: 'Review script for malicious behavior' }); } } checkSecurityScripts(scripts, vulnerabilities) { // Recommend security-related scripts const recommendedScripts = [ { name: 'audit', command: 'npm audit', purpose: 'Run security audit' }, { name: 'audit:fix', command: 'npm audit fix', purpose: 'Fix security vulnerabilities' } ]; for (const { name, command, purpose } of recommendedScripts) { if (!scripts[name]) { vulnerabilities.push({ file: 'package.json', severity: 'low', type: 'missing_security_script', message: `Missing recommended script: "${name}" for ${purpose}`, recommendation: `Add script: "${name}": "${command}"` }); } } } mapNpmSeverity(npmSeverity) { switch (npmSeverity?.toLowerCase()) { case 'critical': return 'critical'; case 'high': return 'high'; case 'moderate': case 'medium': return 'medium'; case 'low': case 'info': default: return 'low'; } } extractVersion(versionString) { // Remove version prefixes return versionString.replace(/^[\^~>=<]+/, ''); } isVersionLessThan(version1, version2) { // Simple version comparison (not perfect but good enough) const v1Parts = version1.split('.').map(Number); const v2Parts = version2.split('.').map(Number); for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { const v1Part = v1Parts[i] || 0; const v2Part = v2Parts[i] || 0; if (v1Part < v2Part) return true; if (v1Part > v2Part) return false; } return false; } isObfuscated(script) { // Check for common obfuscation patterns const obfuscationPatterns = [ /\\x[0-9a-f]{2}/i, // Hex encoding /\\u[0-9a-f]{4}/i, // Unicode encoding /atob\s*\(/, // Base64 decoding /fromCharCode/, // Character code conversion /\b[a-z]{1}\s*=\s*[a-z]{1}\s*\+\s*[a-z]{1}/i, // Single letter variable math /[a-zA-Z0-9+/]{50,}={0,2}/ // Long base64 strings ]; let matches = 0; for (const pattern of obfuscationPatterns) { if (pattern.test(script)) matches++; } return matches >= 2; // Multiple obfuscation indicators } } //# sourceMappingURL=DependencyScanner.js.map