devcompass
Advanced tools
| [ | ||
| { | ||
| "id": "supply-chain", | ||
| "name": "Supply Chain Security", | ||
| "icon": "๐ก๏ธ", | ||
| "priority": 1, | ||
| "description": "Malicious packages, typosquatting, suspicious scripts" | ||
| }, | ||
| { | ||
| "id": "license", | ||
| "name": "License Conflicts", | ||
| "icon": "โ๏ธ", | ||
| "priority": 2, | ||
| "description": "GPL/AGPL/LGPL package replacements" | ||
| }, | ||
| { | ||
| "id": "quality", | ||
| "name": "Package Quality", | ||
| "icon": "๐ฆ", | ||
| "priority": 3, | ||
| "description": "Abandoned, deprecated, stale packages" | ||
| }, | ||
| { | ||
| "id": "security", | ||
| "name": "Critical Security", | ||
| "icon": "๐", | ||
| "priority": 4, | ||
| "description": "npm audit vulnerabilities" | ||
| }, | ||
| { | ||
| "id": "ecosystem", | ||
| "name": "Ecosystem Alerts", | ||
| "icon": "๐จ", | ||
| "priority": 5, | ||
| "description": "Known package issues" | ||
| }, | ||
| { | ||
| "id": "unused", | ||
| "name": "Unused Dependencies", | ||
| "icon": "๐งน", | ||
| "priority": 6, | ||
| "description": "Remove unused packages" | ||
| }, | ||
| { | ||
| "id": "updates", | ||
| "name": "Safe Updates", | ||
| "icon": "โฌ๏ธ", | ||
| "priority": 7, | ||
| "description": "Patch and minor version updates" | ||
| } | ||
| ] |
| { | ||
| "readline-sync": { | ||
| "replacement": "prompts", | ||
| "reason": "MIT licensed alternative" | ||
| }, | ||
| "gnu-which": { | ||
| "replacement": "which", | ||
| "reason": "ISC licensed alternative" | ||
| }, | ||
| "node-forge": { | ||
| "replacement": "forge", | ||
| "reason": "BSD-3-Clause licensed" | ||
| }, | ||
| "gpl-package": { | ||
| "replacement": "mit-alternative", | ||
| "reason": "MIT licensed alternative" | ||
| } | ||
| } |
| { | ||
| "entry": [ | ||
| "src/index.js", | ||
| "index.js", | ||
| "main.js", | ||
| "app.js", | ||
| "server.js" | ||
| ], | ||
| "project": [ | ||
| "**/*.js", | ||
| "**/*.jsx", | ||
| "**/*.ts", | ||
| "**/*.tsx" | ||
| ], | ||
| "ignore": [ | ||
| "node_modules/**", | ||
| "dist/**", | ||
| "build/**", | ||
| ".next/**", | ||
| "coverage/**", | ||
| "out/**", | ||
| "*.min.js" | ||
| ], | ||
| "ignoreDependencies": [ | ||
| "react", | ||
| "react-dom", | ||
| "react-native", | ||
| "next", | ||
| "@angular/core", | ||
| "@angular/common", | ||
| "@angular/platform-browser", | ||
| "@nestjs/core", | ||
| "@nestjs/common", | ||
| "@nestjs/platform-express", | ||
| "typescript", | ||
| "webpack", | ||
| "vite", | ||
| "rollup", | ||
| "esbuild", | ||
| "jest", | ||
| "vitest", | ||
| "mocha", | ||
| "postcss", | ||
| "autoprefixer", | ||
| "tailwindcss", | ||
| "cssnano", | ||
| "prettier", | ||
| "eslint", | ||
| "devcompass" | ||
| ], | ||
| "skipPackages": [ | ||
| "typescript", | ||
| "@types/", | ||
| "eslint", | ||
| "prettier", | ||
| "webpack", | ||
| "vite", | ||
| "rollup", | ||
| "esbuild", | ||
| "jest", | ||
| "vitest", | ||
| "mocha", | ||
| "react", | ||
| "react-dom", | ||
| "next", | ||
| "devcompass" | ||
| ] | ||
| } |
| { | ||
| "AGPL-3.0": { | ||
| "level": "critical", | ||
| "risk": "Requires disclosing source code of network services" | ||
| }, | ||
| "AGPL-3.0-only": { | ||
| "level": "critical", | ||
| "risk": "Requires disclosing source code of network services" | ||
| }, | ||
| "AGPL-3.0-or-later": { | ||
| "level": "critical", | ||
| "risk": "Requires disclosing source code of network services" | ||
| }, | ||
| "GPL-3.0": { | ||
| "level": "high", | ||
| "risk": "Requires derivative works to be GPL licensed" | ||
| }, | ||
| "GPL-3.0-only": { | ||
| "level": "high", | ||
| "risk": "Requires derivative works to be GPL licensed" | ||
| }, | ||
| "GPL-3.0-or-later": { | ||
| "level": "high", | ||
| "risk": "Requires derivative works to be GPL licensed" | ||
| }, | ||
| "GPL-2.0": { | ||
| "level": "high", | ||
| "risk": "Requires derivative works to be GPL licensed" | ||
| }, | ||
| "GPL-2.0-only": { | ||
| "level": "high", | ||
| "risk": "Requires derivative works to be GPL licensed" | ||
| }, | ||
| "GPL-2.0-or-later": { | ||
| "level": "high", | ||
| "risk": "Requires derivative works to be GPL licensed" | ||
| }, | ||
| "LGPL-3.0": { | ||
| "level": "medium", | ||
| "risk": "Modifications to the library must be shared" | ||
| }, | ||
| "LGPL-3.0-only": { | ||
| "level": "medium", | ||
| "risk": "Modifications to the library must be shared" | ||
| }, | ||
| "LGPL-3.0-or-later": { | ||
| "level": "medium", | ||
| "risk": "Modifications to the library must be shared" | ||
| }, | ||
| "LGPL-2.1": { | ||
| "level": "medium", | ||
| "risk": "Modifications to the library must be shared" | ||
| }, | ||
| "LGPL-2.1-only": { | ||
| "level": "medium", | ||
| "risk": "Modifications to the library must be shared" | ||
| }, | ||
| "LGPL-2.1-or-later": { | ||
| "level": "medium", | ||
| "risk": "Modifications to the library must be shared" | ||
| }, | ||
| "MPL-2.0": { | ||
| "level": "medium", | ||
| "risk": "File-level copyleft, changes to MPL files must be shared" | ||
| }, | ||
| "EPL-1.0": { | ||
| "level": "medium", | ||
| "risk": "Weak copyleft, modifications must be shared" | ||
| }, | ||
| "EPL-2.0": { | ||
| "level": "medium", | ||
| "risk": "Weak copyleft, modifications must be shared" | ||
| }, | ||
| "CDDL-1.0": { | ||
| "level": "medium", | ||
| "risk": "File-level copyleft" | ||
| }, | ||
| "MIT": { | ||
| "level": "low", | ||
| "risk": "Permissive - commercial friendly" | ||
| }, | ||
| "ISC": { | ||
| "level": "low", | ||
| "risk": "Permissive - commercial friendly" | ||
| }, | ||
| "BSD-2-Clause": { | ||
| "level": "low", | ||
| "risk": "Permissive - commercial friendly" | ||
| }, | ||
| "BSD-3-Clause": { | ||
| "level": "low", | ||
| "risk": "Permissive - commercial friendly" | ||
| }, | ||
| "Apache-2.0": { | ||
| "level": "low", | ||
| "risk": "Permissive with patent grant" | ||
| }, | ||
| "0BSD": { | ||
| "level": "low", | ||
| "risk": "Public domain equivalent" | ||
| }, | ||
| "Unlicense": { | ||
| "level": "low", | ||
| "risk": "Public domain" | ||
| }, | ||
| "CC0-1.0": { | ||
| "level": "low", | ||
| "risk": "Public domain" | ||
| }, | ||
| "WTFPL": { | ||
| "level": "low", | ||
| "risk": "Permissive" | ||
| } | ||
| } |
| { | ||
| "restrictive": [ | ||
| "GPL", | ||
| "GPL-2.0", | ||
| "GPL-3.0", | ||
| "AGPL", | ||
| "AGPL-3.0", | ||
| "LGPL", | ||
| "LGPL-2.1", | ||
| "LGPL-3.0" | ||
| ], | ||
| "permissive": [ | ||
| "MIT", | ||
| "Apache-2.0", | ||
| "BSD-2-Clause", | ||
| "BSD-3-Clause", | ||
| "ISC", | ||
| "CC0-1.0", | ||
| "Unlicense" | ||
| ] | ||
| } |
| { | ||
| "packages": [ | ||
| "express", | ||
| "react", | ||
| "react-dom", | ||
| "lodash", | ||
| "axios", | ||
| "moment", | ||
| "webpack", | ||
| "typescript", | ||
| "jquery", | ||
| "vue", | ||
| "angular", | ||
| "next", | ||
| "gatsby", | ||
| "nuxt", | ||
| "babel", | ||
| "eslint", | ||
| "prettier", | ||
| "jest", | ||
| "mocha", | ||
| "chai", | ||
| "karma", | ||
| "underscore", | ||
| "request", | ||
| "async", | ||
| "bluebird", | ||
| "chalk", | ||
| "commander", | ||
| "inquirer", | ||
| "ora", | ||
| "yargs", | ||
| "minimist", | ||
| "glob", | ||
| "mkdirp", | ||
| "rimraf", | ||
| "fs-extra", | ||
| "uuid", | ||
| "semver", | ||
| "debug", | ||
| "dotenv", | ||
| "cors", | ||
| "helmet", | ||
| "mongoose", | ||
| "sequelize", | ||
| "knex", | ||
| "prisma", | ||
| "graphql", | ||
| "apollo", | ||
| "socket.io", | ||
| "redis", | ||
| "mysql", | ||
| "pg", | ||
| "sqlite3", | ||
| "mongodb", | ||
| "aws-sdk", | ||
| "firebase", | ||
| "stripe", | ||
| "paypal", | ||
| "twilio", | ||
| "nodemailer", | ||
| "pug", | ||
| "ejs", | ||
| "handlebars", | ||
| "mustache", | ||
| "body-parser", | ||
| "cookie-parser", | ||
| "multer", | ||
| "passport", | ||
| "bcrypt", | ||
| "jsonwebtoken", | ||
| "validator", | ||
| "yup", | ||
| "joi", | ||
| "zod" | ||
| ], | ||
| "whitelist": [ | ||
| "colors", | ||
| "chalk", | ||
| "ora", | ||
| "inquirer", | ||
| "prompts", | ||
| "cross-env", | ||
| "cross-spawn", | ||
| "execa", | ||
| "shelljs", | ||
| "node-fetch", | ||
| "axios", | ||
| "got", | ||
| "request", | ||
| "superagent", | ||
| "nodemailer", | ||
| "sendgrid", | ||
| "mailgun", | ||
| "bcrypt", | ||
| "bcryptjs", | ||
| "argon2", | ||
| "crypto-js", | ||
| "jsonwebtoken", | ||
| "jose", | ||
| "passport", | ||
| "puppeteer", | ||
| "playwright", | ||
| "selenium-webdriver", | ||
| "sharp", | ||
| "jimp", | ||
| "canvas", | ||
| "pdf-lib", | ||
| "esbuild", | ||
| "rollup", | ||
| "vite", | ||
| "parcel", | ||
| "husky", | ||
| "lint-staged", | ||
| "commitlint" | ||
| ] | ||
| } |
| { | ||
| "CRITICAL": { | ||
| "level": 1, | ||
| "label": "CRITICAL", | ||
| "color": "red", | ||
| "emoji": "๐ด" | ||
| }, | ||
| "HIGH": { | ||
| "level": 2, | ||
| "label": "HIGH", | ||
| "color": "orange", | ||
| "emoji": "๐ " | ||
| }, | ||
| "MEDIUM": { | ||
| "level": 3, | ||
| "label": "MEDIUM", | ||
| "color": "yellow", | ||
| "emoji": "๐ก" | ||
| }, | ||
| "LOW": { | ||
| "level": 4, | ||
| "label": "LOW", | ||
| "color": "gray", | ||
| "emoji": "โช" | ||
| } | ||
| } |
| { | ||
| "request": { | ||
| "replacement": "axios", | ||
| "reason": "request is deprecated" | ||
| }, | ||
| "moment": { | ||
| "replacement": "dayjs", | ||
| "reason": "moment is in maintenance mode" | ||
| }, | ||
| "underscore": { | ||
| "replacement": "lodash", | ||
| "reason": "lodash is more actively maintained" | ||
| }, | ||
| "colors": { | ||
| "replacement": "chalk", | ||
| "reason": "colors had a malicious release" | ||
| }, | ||
| "faker": { | ||
| "replacement": "@faker-js/faker", | ||
| "reason": "faker was corrupted, use community fork" | ||
| }, | ||
| "left-pad": { | ||
| "replacement": "string.prototype.padstart", | ||
| "reason": "Use native String methods" | ||
| }, | ||
| "node-uuid": { | ||
| "replacement": "uuid", | ||
| "reason": "node-uuid is deprecated" | ||
| }, | ||
| "querystring": { | ||
| "replacement": "qs", | ||
| "reason": "querystring is legacy" | ||
| }, | ||
| "node-fetch": { | ||
| "replacement": "undici", | ||
| "reason": "undici is faster and maintained by Node.js" | ||
| } | ||
| } |
+1
-1
| { | ||
| "name": "devcompass", | ||
| "version": "3.1.6", | ||
| "version": "3.1.7", | ||
| "description": "Dependency health checker with ecosystem intelligence, user-configurable GitHub Personal Access Token support, real-time GitHub issue tracking for 502 popular npm packages, unified interactive dependency graph with dynamic layout switching, intelligent clustering (Ecosystem/Health/Depth grouping), real-time filtering, advanced zoom controls, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes, backup & rollback, and professional dependency exploration - all in a single interactive HTML file.", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
@@ -9,3 +9,4 @@ // src/analyzers/license-risk.js | ||
| // Extract package names from licenses | ||
| const packageNames = licenses.map(l => l.package); | ||
| const licensesArray = Array.isArray(licenses) ? licenses : (licenses.warnings || []); | ||
| const packageNames = licensesArray.map(l => l.package); | ||
@@ -12,0 +13,0 @@ if (packageNames.length === 0) { |
@@ -5,53 +5,47 @@ // src/analyzers/licenses.js | ||
| // Restrictive licenses that might cause issues | ||
| const RESTRICTIVE_LICENSES = [ | ||
| 'GPL', | ||
| 'GPL-2.0', | ||
| 'GPL-3.0', | ||
| 'AGPL', | ||
| 'AGPL-3.0', | ||
| 'LGPL', | ||
| 'LGPL-2.1', | ||
| 'LGPL-3.0' | ||
| ]; | ||
| // Load license data from JSON | ||
| const licensesData = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/licenses.json'), 'utf8') | ||
| ); | ||
| // Permissive licenses (usually safe) | ||
| const PERMISSIVE_LICENSES = [ | ||
| 'MIT', | ||
| 'Apache-2.0', | ||
| 'BSD-2-Clause', | ||
| 'BSD-3-Clause', | ||
| 'ISC', | ||
| 'CC0-1.0', | ||
| 'Unlicense' | ||
| ]; | ||
| const RESTRICTIVE_LICENSES = licensesData.restrictive; | ||
| const PERMISSIVE_LICENSES = licensesData.permissive; | ||
| /** | ||
| * Check licenses of all dependencies | ||
| */ | ||
| async function checkLicenses(projectPath, dependencies) { | ||
| const licenses = []; | ||
| for (const [packageName, version] of Object.entries(dependencies)) { | ||
| async function analyzeLicenses(dependencies) { | ||
| const warnings = []; | ||
| for (const [name, version] of Object.entries(dependencies)) { | ||
| try { | ||
| const packageJsonPath = path.join( | ||
| projectPath, | ||
| 'node_modules', | ||
| packageName, | ||
| 'package.json' | ||
| ); | ||
| const packagePath = path.join(process.cwd(), 'node_modules', name, 'package.json'); | ||
| if (!fs.existsSync(packageJsonPath)) { | ||
| if (!fs.existsSync(packagePath)) { | ||
| continue; | ||
| } | ||
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); | ||
| const license = packageJson.license || 'UNKNOWN'; | ||
| licenses.push({ | ||
| package: packageName, | ||
| license: license, | ||
| type: getLicenseType(license) | ||
| }); | ||
| const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf-8')); | ||
| const license = packageData.license || 'Unknown'; | ||
| // Check for restrictive licenses | ||
| const isRestrictive = RESTRICTIVE_LICENSES.some(restrictive => | ||
| license.toString().toUpperCase().includes(restrictive) | ||
| ); | ||
| if (isRestrictive) { | ||
| warnings.push({ | ||
| package: name, | ||
| license: license, | ||
| type: 'Restrictive', | ||
| severity: 'medium' | ||
| }); | ||
| } | ||
| // Check for unknown licenses | ||
| if (license === 'Unknown' || license === 'UNLICENSED' || !license) { | ||
| warnings.push({ | ||
| package: name, | ||
| license: license || 'Unknown', | ||
| type: 'Unknown', | ||
| severity: 'low' | ||
| }); | ||
| } | ||
| } catch (error) { | ||
@@ -62,48 +56,28 @@ // Skip packages we can't read | ||
| } | ||
| return licenses; | ||
| } | ||
| /** | ||
| * Determine license type | ||
| */ | ||
| function getLicenseType(license) { | ||
| const licenseStr = String(license).toUpperCase(); | ||
| // Check for restrictive licenses | ||
| for (const restrictive of RESTRICTIVE_LICENSES) { | ||
| if (licenseStr.includes(restrictive)) { | ||
| return 'restrictive'; | ||
| } | ||
| } | ||
| // Check for permissive licenses | ||
| for (const permissive of PERMISSIVE_LICENSES) { | ||
| if (licenseStr.includes(permissive)) { | ||
| return 'permissive'; | ||
| } | ||
| } | ||
| // Unknown or custom license | ||
| if (licenseStr === 'UNKNOWN' || licenseStr === 'UNLICENSED') { | ||
| return 'unknown'; | ||
| } | ||
| return 'other'; | ||
| return { warnings }; | ||
| } | ||
| /** | ||
| * Find problematic licenses | ||
| * ADDED: Find problematic licenses from license array | ||
| * Used by analyze.js for displaying legacy license warnings | ||
| */ | ||
| function findProblematicLicenses(licenses) { | ||
| return licenses.filter(pkg => | ||
| pkg.type === 'restrictive' || pkg.type === 'unknown' | ||
| // Handle both array and object with warnings property | ||
| const licensesArray = Array.isArray(licenses) | ||
| ? licenses | ||
| : (licenses.warnings || []); | ||
| return licensesArray.filter(l => | ||
| l.type === 'Restrictive' || l.type === 'Unknown' | ||
| ); | ||
| } | ||
| module.exports = { | ||
| checkLicenses, | ||
| findProblematicLicenses, | ||
| RESTRICTIVE_LICENSES, | ||
| PERMISSIVE_LICENSES | ||
| // FIXED: Export all expected functions | ||
| module.exports = { | ||
| analyzeLicenses, | ||
| checkLicenses: analyzeLicenses, // Alias for backward compatibility | ||
| findProblematicLicenses, // NEW: Export this function | ||
| RESTRICTIVE_LICENSES, | ||
| PERMISSIVE_LICENSES | ||
| }; |
| // src/analyzers/security-recommendations.js | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
| /** | ||
| * Priority levels for recommendations | ||
| */ | ||
| const PRIORITY = { | ||
| CRITICAL: { level: 1, label: 'CRITICAL', color: 'red', emoji: '๐ด' }, | ||
| HIGH: { level: 2, label: 'HIGH', color: 'orange', emoji: '๐ ' }, | ||
| MEDIUM: { level: 3, label: 'MEDIUM', color: 'yellow', emoji: '๐ก' }, | ||
| LOW: { level: 4, label: 'LOW', color: 'gray', emoji: 'โช' } | ||
| }; | ||
| // Load priorities from JSON | ||
| const PRIORITY = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/priorities.json'), 'utf8') | ||
| ); | ||
| /** | ||
| * Generate actionable recommendations from all security findings | ||
| * Generate prioritized security recommendations based on analysis results | ||
| */ | ||
| function generateSecurityRecommendations(analysisResults) { | ||
| const recommendations = []; | ||
| const { | ||
| supplyChainWarnings = [], | ||
| licenseWarnings = [], | ||
| qualityResults = [], | ||
| securityVulnerabilities = [], | ||
| ecosystemAlerts = [], | ||
| unusedDeps = [], | ||
| outdatedPackages = [] | ||
| supplyChain, | ||
| licenseRisk, | ||
| quality, | ||
| security, | ||
| ecosystem, | ||
| unused, | ||
| outdated | ||
| } = analysisResults; | ||
| // 1. Supply Chain Issues (CRITICAL/HIGH) | ||
| for (const warning of supplyChainWarnings) { | ||
| if (warning.type === 'malicious' || warning.severity === 'critical') { | ||
| recommendations.push({ | ||
| priority: PRIORITY.CRITICAL, | ||
| category: 'supply_chain', | ||
| package: warning.package, | ||
| issue: warning.message, | ||
| action: `Remove ${warning.package} immediately`, | ||
| command: `npm uninstall ${warning.package}`, | ||
| impact: 'Eliminates critical security risk', | ||
| reason: 'Known malicious package detected' | ||
| }); | ||
| } else if (warning.type === 'typosquatting') { | ||
| recommendations.push({ | ||
| priority: PRIORITY.HIGH, | ||
| category: 'supply_chain', | ||
| package: warning.package, | ||
| issue: warning.message, | ||
| action: warning.recommendation, | ||
| command: `npm uninstall ${warning.package} && npm install ${warning.official}`, | ||
| impact: 'Prevents potential supply chain attack', | ||
| reason: 'Typosquatting attempt detected' | ||
| }); | ||
| } else if (warning.type === 'install_script' && warning.severity === 'high') { | ||
| recommendations.push({ | ||
| priority: PRIORITY.HIGH, | ||
| category: 'supply_chain', | ||
| package: warning.package, | ||
| issue: warning.message, | ||
| action: 'Review install script before deployment', | ||
| command: `cat node_modules/${warning.package.split('@')[0]}/package.json`, | ||
| impact: 'Prevents malicious code execution', | ||
| reason: 'Suspicious install script detected' | ||
| }); | ||
| } | ||
| // 1. CRITICAL: Supply chain threats | ||
| if (supplyChain && supplyChain.warnings) { | ||
| supplyChain.warnings.forEach(warning => { | ||
| if (warning.type === 'malicious') { | ||
| recommendations.push({ | ||
| priority: PRIORITY.CRITICAL, | ||
| category: 'supply_chain', | ||
| title: 'Remove malicious package', | ||
| package: warning.package, | ||
| action: `npm uninstall ${warning.package}`, | ||
| reason: warning.description, | ||
| impact: 'Critical security risk eliminated' | ||
| }); | ||
| } else if (warning.type === 'typosquat') { | ||
| recommendations.push({ | ||
| priority: PRIORITY.HIGH, | ||
| category: 'supply_chain', | ||
| title: 'Replace typosquatting attempt', | ||
| package: warning.package, | ||
| action: `npm uninstall ${warning.package} && npm install ${warning.official}`, | ||
| reason: `Similar to legitimate package: ${warning.official}`, | ||
| impact: 'Prevent potential security breach' | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| // 2. License Compliance (HIGH/MEDIUM) | ||
| for (const warning of licenseWarnings) { | ||
| if (warning.severity === 'critical' || warning.severity === 'high') { | ||
| recommendations.push({ | ||
| priority: warning.severity === 'critical' ? PRIORITY.CRITICAL : PRIORITY.HIGH, | ||
| category: 'license', | ||
| package: warning.package, | ||
| issue: `${warning.license}: ${warning.message}`, | ||
| action: 'Replace with permissive alternative', | ||
| command: `npm uninstall ${warning.package}`, | ||
| impact: 'Ensures license compliance', | ||
| reason: 'High-risk license detected', | ||
| alternative: 'Search npm for MIT/Apache alternatives' | ||
| }); | ||
| } | ||
| // 2. HIGH: License conflicts | ||
| if (licenseRisk && licenseRisk.warnings) { | ||
| licenseRisk.warnings.forEach(warning => { | ||
| if (warning.autoFixable) { | ||
| recommendations.push({ | ||
| priority: PRIORITY.HIGH, | ||
| category: 'license', | ||
| title: 'Resolve license conflict', | ||
| package: warning.package, | ||
| action: warning.suggestedAlternative | ||
| ? `npm uninstall ${warning.package} && npm install ${warning.suggestedAlternative}` | ||
| : `Review license compatibility for ${warning.package}`, | ||
| reason: `${warning.license} license may conflict with commercial use`, | ||
| impact: 'Legal compliance ensured' | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| // 3. Security Vulnerabilities (CRITICAL/HIGH) | ||
| if (securityVulnerabilities.critical > 0 || securityVulnerabilities.high > 0) { | ||
| recommendations.push({ | ||
| priority: securityVulnerabilities.critical > 0 ? PRIORITY.CRITICAL : PRIORITY.HIGH, | ||
| category: 'security', | ||
| issue: `${securityVulnerabilities.total} security vulnerabilities detected`, | ||
| action: 'Run npm audit fix to resolve vulnerabilities', | ||
| command: 'npm audit fix', | ||
| impact: `Resolves ${securityVulnerabilities.total} known vulnerabilities`, | ||
| reason: 'Security vulnerabilities in dependencies' | ||
| // 3. HIGH: Abandoned/deprecated packages | ||
| if (quality && quality.packages) { | ||
| quality.packages.forEach(pkg => { | ||
| if (pkg.status === 'deprecated' || pkg.status === 'abandoned') { | ||
| const alternative = pkg.alternative || 'Find actively maintained alternative'; | ||
| recommendations.push({ | ||
| priority: PRIORITY.HIGH, | ||
| category: 'quality', | ||
| title: `Replace ${pkg.status} package`, | ||
| package: pkg.name, | ||
| action: typeof alternative === 'string' | ||
| ? alternative | ||
| : `npm uninstall ${pkg.name} && npm install ${alternative}`, | ||
| reason: `Package is ${pkg.status}`, | ||
| impact: 'Improved stability and security' | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| // 4. Ecosystem Alerts (varies by severity) | ||
| for (const alert of ecosystemAlerts) { | ||
| if (alert.severity === 'critical' || alert.severity === 'high') { | ||
| const priority = alert.severity === 'critical' ? PRIORITY.CRITICAL : PRIORITY.HIGH; | ||
| // 4. MEDIUM: Security vulnerabilities | ||
| if (security && security.vulnerabilities && security.vulnerabilities.length > 0) { | ||
| const criticalVulns = security.vulnerabilities.filter(v => v.severity === 'critical').length; | ||
| const highVulns = security.vulnerabilities.filter(v => v.severity === 'high').length; | ||
| if (criticalVulns > 0 || highVulns > 0) { | ||
| recommendations.push({ | ||
| priority: priority, | ||
| category: 'ecosystem', | ||
| package: `${alert.package}@${alert.version}`, | ||
| issue: alert.title, | ||
| action: `Upgrade to ${alert.fix}`, | ||
| command: `npm install ${alert.package}@${alert.fix}`, | ||
| impact: 'Resolves known stability/security issue', | ||
| reason: alert.source | ||
| }); | ||
| } | ||
| } | ||
| // 5. Package Quality Issues (MEDIUM/LOW) | ||
| for (const pkg of qualityResults) { | ||
| if (pkg.status === 'deprecated') { | ||
| recommendations.push({ | ||
| priority: PRIORITY.CRITICAL, | ||
| category: 'quality', | ||
| package: pkg.package, | ||
| issue: 'Package is deprecated', | ||
| action: 'Find actively maintained alternative', | ||
| command: `npm uninstall ${pkg.package}`, | ||
| impact: 'Prevents future breaking changes', | ||
| reason: 'Package is no longer maintained', | ||
| healthScore: pkg.healthScore | ||
| category: 'security', | ||
| title: 'Fix security vulnerabilities', | ||
| package: 'multiple', | ||
| action: 'npm audit fix', | ||
| reason: `${criticalVulns} critical, ${highVulns} high severity vulnerabilities`, | ||
| impact: 'Security vulnerabilities resolved' | ||
| }); | ||
| } else if (pkg.status === 'abandoned') { | ||
| recommendations.push({ | ||
| priority: PRIORITY.HIGH, | ||
| category: 'quality', | ||
| package: pkg.package, | ||
| issue: `Last updated ${Math.floor(pkg.daysSincePublish / 365)} years ago`, | ||
| action: 'Migrate to actively maintained alternative', | ||
| command: `npm uninstall ${pkg.package}`, | ||
| impact: 'Improves long-term stability', | ||
| reason: 'Package appears abandoned', | ||
| healthScore: pkg.healthScore | ||
| }); | ||
| } else if (pkg.status === 'stale' && pkg.healthScore < 5) { | ||
| recommendations.push({ | ||
| priority: PRIORITY.MEDIUM, | ||
| category: 'quality', | ||
| package: pkg.package, | ||
| issue: `Health score: ${pkg.healthScore}/10`, | ||
| action: 'Consider more actively maintained alternative', | ||
| impact: 'Improves package quality', | ||
| reason: 'Low health score', | ||
| healthScore: pkg.healthScore | ||
| }); | ||
| } | ||
| } | ||
| // 6. Unused Dependencies (MEDIUM) | ||
| if (unusedDeps.length > 0) { | ||
| const packageList = unusedDeps.map(d => d.name).join(' '); | ||
| // 5. MEDIUM: Ecosystem alerts | ||
| if (ecosystem && ecosystem.alerts && ecosystem.alerts.length > 0) { | ||
| ecosystem.alerts.forEach(alert => { | ||
| if (alert.severity === 'high' || alert.severity === 'critical') { | ||
| recommendations.push({ | ||
| priority: alert.severity === 'critical' ? PRIORITY.CRITICAL : PRIORITY.HIGH, | ||
| category: 'ecosystem', | ||
| title: 'Address known package issue', | ||
| package: alert.package, | ||
| action: alert.fix ? `npm install ${alert.package}@${alert.fix}` : `Review ${alert.package}`, | ||
| reason: alert.issue, | ||
| impact: 'Known issues resolved' | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| // 6. LOW: Unused dependencies | ||
| if (unused && unused.length > 0) { | ||
| recommendations.push({ | ||
| priority: PRIORITY.MEDIUM, | ||
| category: 'cleanup', | ||
| issue: `${unusedDeps.length} unused dependencies detected`, | ||
| action: 'Remove unused packages', | ||
| command: `npm uninstall ${packageList}`, | ||
| impact: `Reduces node_modules size, improves security surface`, | ||
| reason: 'Unused dependencies increase attack surface' | ||
| title: 'Clean up unused dependencies', | ||
| package: 'multiple', | ||
| action: `npm uninstall ${unused.join(' ')}`, | ||
| reason: `${unused.length} unused dependencies detected`, | ||
| impact: 'Reduced bundle size and maintenance burden' | ||
| }); | ||
| } | ||
| // 7. Outdated Packages (LOW) | ||
| const criticalOutdated = outdatedPackages.filter(p => | ||
| p.updateType === 'major update' && p.current.startsWith('0.') | ||
| ); | ||
| if (criticalOutdated.length > 0) { | ||
| for (const pkg of criticalOutdated.slice(0, 3)) { // Top 3 | ||
| // 7. LOW: Outdated packages | ||
| if (outdated && outdated.length > 0) { | ||
| const safeUpdates = outdated.filter(pkg => pkg.updateType === 'patch' || pkg.updateType === 'minor'); | ||
| if (safeUpdates.length > 0) { | ||
| recommendations.push({ | ||
| priority: PRIORITY.MEDIUM, | ||
| priority: PRIORITY.LOW, | ||
| category: 'updates', | ||
| package: pkg.name, | ||
| issue: `Version ${pkg.current} is pre-1.0 and outdated`, | ||
| action: `Update to ${pkg.latest}`, | ||
| command: `npm install ${pkg.name}@latest`, | ||
| impact: 'Gets bug fixes and improvements', | ||
| reason: 'Pre-1.0 packages change rapidly' | ||
| title: 'Apply safe updates', | ||
| package: 'multiple', | ||
| action: 'npm update', | ||
| reason: `${safeUpdates.length} packages have safe updates available`, | ||
| impact: 'Bug fixes and minor improvements' | ||
| }); | ||
| } | ||
| } | ||
| // Sort by priority | ||
| // Sort by priority level | ||
| recommendations.sort((a, b) => a.priority.level - b.priority.level); | ||
| return recommendations; | ||
@@ -196,70 +163,22 @@ } | ||
| /** | ||
| * Group recommendations by priority | ||
| * Calculate expected health score improvement | ||
| */ | ||
| function groupByPriority(recommendations) { | ||
| return { | ||
| critical: recommendations.filter(r => r.priority.level === 1), | ||
| high: recommendations.filter(r => r.priority.level === 2), | ||
| medium: recommendations.filter(r => r.priority.level === 3), | ||
| low: recommendations.filter(r => r.priority.level === 4) | ||
| }; | ||
| } | ||
| function calculateExpectedImprovement(currentScore, recommendations) { | ||
| let improvement = 0; | ||
| /** | ||
| * Calculate expected impact of following recommendations | ||
| */ | ||
| function calculateExpectedImpact(recommendations, currentScore) { | ||
| let improvement = 0; | ||
| for (const rec of recommendations) { | ||
| recommendations.forEach(rec => { | ||
| switch (rec.priority.level) { | ||
| case 1: // CRITICAL | ||
| improvement += 2.0; | ||
| break; | ||
| case 2: // HIGH | ||
| improvement += 1.5; | ||
| break; | ||
| case 3: // MEDIUM | ||
| improvement += 0.5; | ||
| break; | ||
| case 4: // LOW | ||
| improvement += 0.2; | ||
| break; | ||
| case 1: improvement += 2.0; break; // CRITICAL | ||
| case 2: improvement += 1.5; break; // HIGH | ||
| case 3: improvement += 0.5; break; // MEDIUM | ||
| case 4: improvement += 0.2; break; // LOW | ||
| } | ||
| } | ||
| const newScore = Math.min(10, currentScore + improvement); | ||
| const percentageIncrease = ((newScore - currentScore) / 10) * 100; | ||
| return { | ||
| currentScore, | ||
| expectedScore: Number(newScore.toFixed(1)), | ||
| improvement: Number(improvement.toFixed(1)), | ||
| percentageIncrease: Number(percentageIncrease.toFixed(1)), | ||
| critical: recommendations.filter(r => r.priority.level === 1).length, | ||
| high: recommendations.filter(r => r.priority.level === 2).length, | ||
| medium: recommendations.filter(r => r.priority.level === 3).length, | ||
| low: recommendations.filter(r => r.priority.level === 4).length | ||
| }; | ||
| } | ||
| }); | ||
| /** | ||
| * Get top N recommendations | ||
| */ | ||
| function getTopRecommendations(recommendations, n = 5) { | ||
| return recommendations.slice(0, n); | ||
| } | ||
| /** | ||
| * Get recommendations by category | ||
| */ | ||
| function getRecommendationsByCategory(recommendations) { | ||
| const expectedScore = Math.min(currentScore + improvement, 10); | ||
| return { | ||
| supply_chain: recommendations.filter(r => r.category === 'supply_chain'), | ||
| license: recommendations.filter(r => r.category === 'license'), | ||
| security: recommendations.filter(r => r.category === 'security'), | ||
| ecosystem: recommendations.filter(r => r.category === 'ecosystem'), | ||
| quality: recommendations.filter(r => r.category === 'quality'), | ||
| cleanup: recommendations.filter(r => r.category === 'cleanup'), | ||
| updates: recommendations.filter(r => r.category === 'updates') | ||
| current: currentScore, | ||
| expected: expectedScore, | ||
| improvement: improvement, | ||
| improvementPercent: Math.round((improvement / 10) * 100) | ||
| }; | ||
@@ -270,7 +189,4 @@ } | ||
| generateSecurityRecommendations, | ||
| groupByPriority, | ||
| calculateExpectedImpact, | ||
| getTopRecommendations, | ||
| getRecommendationsByCategory, | ||
| calculateExpectedImprovement, | ||
| PRIORITY | ||
| }; |
@@ -51,9 +51,15 @@ // src/analyzers/supply-chain.js | ||
| try { | ||
| auditResults = analyzer.security.runNpmAudit(projectPath); | ||
| auditResults = await analyzer.security.runNpmAudit(projectPath); | ||
| } catch (error) { | ||
| // npm audit may fail in some environments, continue anyway | ||
| console.error('[supply-chain] npm audit failed:', error.message); | ||
| } | ||
| // โ FIXED: Safe iteration over vulnerabilities | ||
| const vulnArray = Array.isArray(auditResults.vulnerabilities) | ||
| ? auditResults.vulnerabilities | ||
| : []; | ||
| // Add high/critical vulnerabilities to warnings | ||
| for (const vuln of auditResults.vulnerabilities) { | ||
| for (const vuln of vulnArray) { | ||
| const severity = (vuln.severity || 'moderate').toLowerCase(); | ||
@@ -63,7 +69,7 @@ | ||
| warnings.push({ | ||
| package: vuln.package, | ||
| package: vuln.package || 'unknown', | ||
| type: 'vulnerability', | ||
| severity: severity, | ||
| description: vuln.title, | ||
| reason: vuln.title, | ||
| description: vuln.title || vuln.via || 'Security vulnerability detected', | ||
| reason: vuln.title || vuln.via || 'Security vulnerability', | ||
| risk: `${severity.toUpperCase()} security vulnerability`, | ||
@@ -82,5 +88,5 @@ action: 'update', | ||
| suspiciousScripts: warnings.filter(w => w.type === 'install_script').length, | ||
| vulnerabilities: auditResults.summary.total, | ||
| critical: auditResults.summary.critical || 0, | ||
| high: auditResults.summary.high || 0 | ||
| vulnerabilities: auditResults.summary?.total || 0, | ||
| critical: auditResults.summary?.critical || 0, | ||
| high: auditResults.summary?.high || 0 | ||
| }; | ||
@@ -113,3 +119,2 @@ | ||
| function removeFromWhitelist(packageName) { | ||
@@ -119,3 +124,2 @@ analyzer.security.removeFromWhitelist(packageName); | ||
| function isWhitelisted(packageName) { | ||
@@ -122,0 +126,0 @@ return analyzer.security.isWhitelisted(packageName); |
+73
-329
| // src/analyzers/unused-deps.js | ||
| const { execSync } = require('child_process'); | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| // Load knip configuration from JSON | ||
| const knipConfigData = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/knip-config.json'), 'utf8') | ||
| ); | ||
| async function findUnusedDeps(projectPath, dependencies) { | ||
| // โ FIXED: Validate inputs | ||
| if (!projectPath || typeof projectPath !== 'string') { | ||
| console.error('Invalid project path provided to findUnusedDeps'); | ||
| return []; | ||
| } | ||
| async function analyzeUnusedDependencies(projectPath) { | ||
| try { | ||
| const packageJsonPath = path.join(projectPath, 'package.json'); | ||
| if (!fs.existsSync(packageJsonPath)) { | ||
| return []; | ||
| } | ||
| if (!dependencies || typeof dependencies !== 'object') { | ||
| console.error('Invalid dependencies object provided to findUnusedDeps'); | ||
| return []; | ||
| } | ||
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); | ||
| const dependencies = { | ||
| ...packageJson.dependencies, | ||
| ...packageJson.devDependencies | ||
| }; | ||
| try { | ||
| // Create a temporary knip config if it doesn't exist | ||
| // Create knip config if it doesn't exist | ||
| const knipConfigPath = path.join(projectPath, 'knip.json'); | ||
| let hasKnipConfig = false; | ||
| // โ FIXED: Safe file existence check | ||
| try { | ||
| hasKnipConfig = fs.existsSync(knipConfigPath); | ||
| } catch (error) { | ||
| console.error('Warning: Could not check for knip config:', error.message); | ||
| hasKnipConfig = false; | ||
| if (!fs.existsSync(knipConfigPath)) { | ||
| fs.writeFileSync( | ||
| knipConfigPath, | ||
| JSON.stringify(knipConfigData, null, 2) | ||
| ); | ||
| } | ||
| // If no knip config exists, create a minimal one | ||
| if (!hasKnipConfig) { | ||
| const minimalConfig = { | ||
| entry: ['src/index.js', 'index.js', 'main.js', 'app.js', 'server.js'], | ||
| project: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], | ||
| ignore: [ | ||
| 'node_modules/**', | ||
| 'dist/**', | ||
| 'build/**', | ||
| '.next/**', | ||
| 'coverage/**', | ||
| 'out/**', | ||
| '*.min.js' | ||
| ], | ||
| ignoreDependencies: [ | ||
| // Framework core packages that may not be directly imported | ||
| 'react', | ||
| 'react-dom', | ||
| 'react-native', | ||
| 'next', | ||
| '@angular/core', | ||
| '@angular/common', | ||
| '@angular/platform-browser', | ||
| '@nestjs/core', | ||
| '@nestjs/common', | ||
| '@nestjs/platform-express', | ||
| 'typescript', | ||
| // Build tools used in config files | ||
| 'webpack', | ||
| 'vite', | ||
| 'rollup', | ||
| 'esbuild', | ||
| // Testing frameworks | ||
| 'jest', | ||
| 'vitest', | ||
| 'mocha', | ||
| // CSS/PostCSS (used in config files) | ||
| 'postcss', | ||
| 'autoprefixer', | ||
| 'tailwindcss', | ||
| 'cssnano', | ||
| // Linting/Formatting (used in config files) | ||
| 'prettier', | ||
| 'eslint', | ||
| // Self-reference | ||
| 'devcompass' | ||
| ] | ||
| }; | ||
| // Write temporary config with error handling | ||
| try { | ||
| fs.writeFileSync(knipConfigPath, JSON.stringify(minimalConfig, null, 2)); | ||
| } catch (writeError) { | ||
| console.error('Warning: Could not create temporary knip config:', writeError.message); | ||
| // Continue without config - knip will use defaults | ||
| } | ||
| } | ||
| // Run knip with JSON reporter | ||
| const knipCommand = `npx knip --no-progress --reporter json`; | ||
| let result; | ||
| try { | ||
| result = execSync(knipCommand, { | ||
| // Run knip to detect unused dependencies | ||
| const knipOutput = execSync('npx knip --reporter json', { | ||
| cwd: projectPath, | ||
| encoding: 'utf8', | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| timeout: 30000, // 30 second timeout | ||
| maxBuffer: 10 * 1024 * 1024 // 10MB buffer | ||
| encoding: 'utf-8', | ||
| timeout: 30000, | ||
| stdio: ['pipe', 'pipe', 'pipe'] | ||
| }); | ||
| } catch (execError) { | ||
| // Knip exits with code 1 if it finds issues, which is expected | ||
| // We still want to parse the output | ||
| if (execError.stdout) { | ||
| result = execError.stdout; | ||
| } else { | ||
| throw execError; | ||
| const results = JSON.parse(knipOutput); | ||
| let unused = []; | ||
| // Parse knip output | ||
| if (results.issues) { | ||
| unused = results.issues | ||
| .filter(issue => issue.type === 'unlisted' || issue.type === 'unresolved') | ||
| .map(issue => issue.symbol) | ||
| .filter(dep => dependencies[dep]); | ||
| } | ||
| } finally { | ||
| // Clean up temporary config if we created it | ||
| if (!hasKnipConfig) { | ||
| try { | ||
| if (fs.existsSync(knipConfigPath)) { | ||
| fs.unlinkSync(knipConfigPath); | ||
| } | ||
| } catch (cleanupError) { | ||
| // Ignore cleanup errors - file system might be locked | ||
| console.error('Warning: Could not clean up temporary knip config:', cleanupError.message); | ||
| } | ||
| } | ||
| } | ||
| // โ FIXED: Validate result before parsing | ||
| if (!result || typeof result !== 'string') { | ||
| console.error('Warning: Knip returned invalid output'); | ||
| return fallbackUnusedCheck(projectPath, dependencies); | ||
| } | ||
| // Filter out packages from skipPackages | ||
| unused = unused.filter(pkg => | ||
| !knipConfigData.skipPackages.some(skip => pkg.includes(skip)) | ||
| ); | ||
| // Parse knip JSON output | ||
| let knipResults; | ||
| try { | ||
| knipResults = JSON.parse(result); | ||
| } catch (parseError) { | ||
| console.error('Warning: Could not parse knip output:', parseError.message); | ||
| // โ RETURN STRINGS (not objects) - this is what the code expects | ||
| return unused; | ||
| } catch (knipError) { | ||
| // Fallback if knip fails | ||
| console.error('Knip failed, using fallback mechanism'); | ||
| return fallbackUnusedCheck(projectPath, dependencies); | ||
| } | ||
| // โ FIXED: Validate knipResults is an object | ||
| if (!knipResults || typeof knipResults !== 'object') { | ||
| console.error('Warning: Knip returned invalid results structure'); | ||
| return fallbackUnusedCheck(projectPath, dependencies); | ||
| } | ||
| // Extract unused dependencies from knip results | ||
| const unusedDeps = []; | ||
| const seenDeps = new Set(); | ||
| // Knip output structure varies, handle different formats | ||
| if (knipResults.issues && typeof knipResults.issues === 'object') { | ||
| // Format 1: issues object with file paths as keys | ||
| for (const [file, issues] of Object.entries(knipResults.issues)) { | ||
| // โ FIXED: Validate issues object | ||
| if (!issues || typeof issues !== 'object') continue; | ||
| if (Array.isArray(issues.dependencies)) { | ||
| issues.dependencies.forEach(dep => { | ||
| // Handle both string and object formats | ||
| const depName = typeof dep === 'string' ? dep : (dep?.name || null); | ||
| if (typeof depName === 'string' && depName.length > 0 && !seenDeps.has(depName)) { | ||
| seenDeps.add(depName); | ||
| unusedDeps.push({ name: depName }); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| } else if (Array.isArray(knipResults.dependencies)) { | ||
| // Format 2: direct dependencies array | ||
| knipResults.dependencies.forEach(dep => { | ||
| const depName = typeof dep === 'string' ? dep : (dep?.name || null); | ||
| if (typeof depName === 'string' && depName.length > 0 && !seenDeps.has(depName)) { | ||
| seenDeps.add(depName); | ||
| unusedDeps.push({ name: depName }); | ||
| } | ||
| }); | ||
| } else if (Array.isArray(knipResults.files)) { | ||
| // Format 3: files array with dependency info | ||
| knipResults.files.forEach(file => { | ||
| // โ FIXED: Validate file object | ||
| if (!file || typeof file !== 'object') return; | ||
| if (Array.isArray(file.dependencies)) { | ||
| file.dependencies.forEach(dep => { | ||
| const depName = typeof dep === 'string' ? dep : (dep?.name || null); | ||
| if (typeof depName === 'string' && depName.length > 0 && !seenDeps.has(depName)) { | ||
| seenDeps.add(depName); | ||
| unusedDeps.push({ name: depName }); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| // Filter out @types packages and common false positives | ||
| const filtered = unusedDeps.filter(dep => { | ||
| // โ FIXED: Validate dep object | ||
| if (!dep || typeof dep !== 'object') { | ||
| return false; | ||
| } | ||
| const name = dep.name; | ||
| // Safety check: ensure name is a string | ||
| if (typeof name !== 'string' || name.length === 0) { | ||
| return false; | ||
| } | ||
| // Keep all non-@types packages | ||
| if (!name.startsWith('@types/')) { | ||
| return true; | ||
| } | ||
| // For @types packages, only flag if the base package is also unused | ||
| const baseName = name.replace('@types/', ''); | ||
| const hasBasePackage = dependencies[baseName] !== undefined; | ||
| const baseIsUnused = unusedDeps.some(d => d?.name === baseName); | ||
| // Only flag @types if base package exists and is also unused | ||
| return hasBasePackage && baseIsUnused; | ||
| }); | ||
| return filtered; | ||
| } catch (error) { | ||
| // If knip fails completely, fall back to empty array | ||
| // This prevents the entire analysis from failing | ||
| console.error('Warning: Knip analysis failed:', error.message); | ||
| // Try a simple fallback: check for obviously unused deps | ||
| return fallbackUnusedCheck(projectPath, dependencies); | ||
| console.error('Unused dependencies analysis failed:', error.message); | ||
| return []; | ||
| } | ||
@@ -240,126 +75,35 @@ } | ||
| /** | ||
| * Fallback method if knip fails | ||
| * Simple check for packages that are never imported | ||
| * Fallback mechanism when knip fails | ||
| */ | ||
| function fallbackUnusedCheck(projectPath, dependencies) { | ||
| // โ FIXED: Validate inputs | ||
| if (!projectPath || typeof projectPath !== 'string') { | ||
| console.error('Invalid project path in fallback check'); | ||
| return []; | ||
| } | ||
| const unused = []; | ||
| if (!dependencies || typeof dependencies !== 'object') { | ||
| console.error('Invalid dependencies in fallback check'); | ||
| return []; | ||
| } | ||
| for (const dep of Object.keys(dependencies)) { | ||
| // Skip packages from skipPackages | ||
| if (knipConfigData.skipPackages.some(skip => dep.includes(skip))) { | ||
| continue; | ||
| } | ||
| try { | ||
| const unusedDeps = []; | ||
| // Read all JS/TS files | ||
| let projectFiles = []; | ||
| try { | ||
| const findResult = execSync( | ||
| 'find . -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" | grep -v node_modules', | ||
| { | ||
| cwd: projectPath, | ||
| encoding: 'utf8', | ||
| timeout: 10000, // 10 second timeout | ||
| maxBuffer: 5 * 1024 * 1024 // 5MB buffer | ||
| } | ||
| ); | ||
| // โ FIXED: Validate findResult | ||
| if (typeof findResult === 'string') { | ||
| projectFiles = findResult.split('\n').filter(file => | ||
| typeof file === 'string' && file.length > 0 | ||
| ); | ||
| } | ||
| } catch (findError) { | ||
| console.error('Warning: Could not find project files:', findError.message); | ||
| return []; | ||
| } | ||
| // Simple check: does package appear in any JS/TS files? | ||
| const grepCmd = `grep -r "${dep}" ${projectPath}/src ${projectPath}/index.js ${projectPath}/main.js 2>/dev/null || true`; | ||
| const result = execSync(grepCmd, { encoding: 'utf-8', timeout: 5000 }); | ||
| // โ FIXED: Return early if no files found | ||
| if (projectFiles.length === 0) { | ||
| console.error('Warning: No project files found for unused dependency check'); | ||
| return []; | ||
| } | ||
| // Read all file contents | ||
| let allContent = ''; | ||
| projectFiles.forEach(file => { | ||
| try { | ||
| const filePath = path.join(projectPath, file); | ||
| const content = fs.readFileSync(filePath, 'utf8'); | ||
| // โ FIXED: Validate content | ||
| if (typeof content === 'string') { | ||
| allContent += content; | ||
| } | ||
| } catch (err) { | ||
| // Skip files that can't be read | ||
| if (!result || result.trim().length === 0) { | ||
| unused.push(dep); | ||
| } | ||
| }); | ||
| // โ FIXED: Return early if no content | ||
| if (allContent.length === 0) { | ||
| console.error('Warning: No content found in project files'); | ||
| return []; | ||
| } catch { | ||
| // If grep fails, skip this package | ||
| continue; | ||
| } | ||
| } | ||
| // Check each dependency | ||
| Object.keys(dependencies).forEach(dep => { | ||
| // โ FIXED: Validate dependency name | ||
| if (typeof dep !== 'string' || dep.length === 0) { | ||
| return; | ||
| } | ||
| // Skip certain packages that are known to be used indirectly | ||
| const skipPackages = [ | ||
| 'typescript', '@types/', 'eslint', 'prettier', | ||
| 'webpack', 'vite', 'rollup', 'esbuild', | ||
| 'jest', 'vitest', 'mocha', | ||
| 'react', 'react-dom', 'next', | ||
| 'devcompass' | ||
| ]; | ||
| const shouldSkip = skipPackages.some(skip => | ||
| dep === skip || dep.startsWith(skip) | ||
| ); | ||
| if (shouldSkip) { | ||
| return; | ||
| } | ||
| // โ FIXED: Wrap regex creation in try-catch | ||
| try { | ||
| // Escape special regex characters in package name | ||
| const escapedDep = dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
| // Check if package is imported anywhere | ||
| const requirePattern = new RegExp(`require\\(['"\`]${escapedDep}['"\`]\\)`, 'g'); | ||
| const importPattern = new RegExp(`import .* from ['"\`]${escapedDep}['"\`]`, 'g'); | ||
| const hasRequire = requirePattern.test(allContent); | ||
| const hasImport = importPattern.test(allContent); | ||
| if (!hasRequire && !hasImport) { | ||
| unusedDeps.push({ name: dep }); | ||
| } | ||
| } catch (regexError) { | ||
| // Skip packages that cause regex errors (likely malformed names) | ||
| console.error(`Warning: Could not check dependency "${dep}":`, regexError.message); | ||
| } | ||
| }); | ||
| return unusedDeps; | ||
| } catch (fallbackError) { | ||
| console.error('Warning: Fallback unused check failed:', fallbackError.message); | ||
| return []; | ||
| } | ||
| // โ RETURN STRINGS (not objects) | ||
| return unused; | ||
| } | ||
| module.exports = { findUnusedDeps }; | ||
| // Export all expected function names | ||
| module.exports = { | ||
| analyzeUnusedDependencies, | ||
| findUnusedDeps: analyzeUnusedDependencies // Alias for backward compatibility | ||
| }; |
@@ -947,4 +947,6 @@ // src/commands/analyze.js | ||
| unusedDeps.forEach(dep => { | ||
| log(` ${chalk.red('โ')} ${dep.name}`); | ||
| unusedDeps.forEach(dep => { | ||
| // โ FIXED: Handle both string and object formats | ||
| const depName = typeof dep === 'string' ? dep : (dep.name || dep.package || dep); | ||
| log(` ${chalk.red('โ')} ${typeof dep === 'string' ? dep : (dep.name || dep)}`); | ||
| }); | ||
@@ -951,0 +953,0 @@ |
+101
-232
| // src/services/dynamic-license.js | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
| const registryClient = require('./registry-client'); | ||
| // License risk levels | ||
| const LICENSE_RISKS = { | ||
| // Critical - Viral copyleft, requires entire codebase to be open source | ||
| 'AGPL-3.0': { level: 'critical', risk: 'Requires disclosing source code of network services' }, | ||
| 'AGPL-3.0-only': { level: 'critical', risk: 'Requires disclosing source code of network services' }, | ||
| 'AGPL-3.0-or-later': { level: 'critical', risk: 'Requires disclosing source code of network services' }, | ||
| // High - Strong copyleft | ||
| 'GPL-3.0': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-3.0-only': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-3.0-or-later': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-2.0': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-2.0-only': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-2.0-or-later': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| // Medium - Weak copyleft | ||
| 'LGPL-3.0': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-3.0-only': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-3.0-or-later': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-2.1': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-2.1-only': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-2.1-or-later': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'MPL-2.0': { level: 'medium', risk: 'File-level copyleft, changes to MPL files must be shared' }, | ||
| 'EPL-1.0': { level: 'medium', risk: 'Weak copyleft, modifications must be shared' }, | ||
| 'EPL-2.0': { level: 'medium', risk: 'Weak copyleft, modifications must be shared' }, | ||
| 'CDDL-1.0': { level: 'medium', risk: 'File-level copyleft' }, | ||
| // Low - Permissive licenses (safe for commercial use) | ||
| 'MIT': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'ISC': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'BSD-2-Clause': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'BSD-3-Clause': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'Apache-2.0': { level: 'low', risk: 'Permissive with patent grant' }, | ||
| '0BSD': { level: 'low', risk: 'Public domain equivalent' }, | ||
| 'Unlicense': { level: 'low', risk: 'Public domain' }, | ||
| 'CC0-1.0': { level: 'low', risk: 'Public domain' }, | ||
| 'WTFPL': { level: 'low', risk: 'Permissive' } | ||
| }; | ||
| // Load license risk data from JSON | ||
| const LICENSE_RISKS = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/license-risks.json'), 'utf8') | ||
| ); | ||
| // Known alternatives for GPL packages | ||
| const GPL_ALTERNATIVES = { | ||
| 'readline-sync': { replacement: 'prompts', reason: 'MIT licensed alternative' }, | ||
| 'gnu-which': { replacement: 'which', reason: 'ISC licensed alternative' }, | ||
| 'node-forge': { replacement: 'forge', reason: 'BSD-3-Clause licensed' }, | ||
| 'gpl-package': { replacement: 'mit-alternative', reason: 'MIT licensed alternative' } | ||
| }; | ||
| const GPL_ALTERNATIVES = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/gpl-alternatives.json'), 'utf8') | ||
| ); | ||
| /** | ||
| * Analyze a package's license | ||
| */ | ||
| async function analyzePackage(packageName, version) { | ||
| try { | ||
| const packageData = await registryClient.getPackageData(packageName, version); | ||
| if (!packageData) { | ||
| return null; | ||
| } | ||
| function normalizeLicense(license) { | ||
| if (!license) return 'UNKNOWN'; | ||
| // Handle common variations | ||
| const normalized = license | ||
| .replace(/\s+/g, '-') | ||
| .replace(/^Apache 2\.0$/i, 'Apache-2.0') | ||
| .replace(/^Apache-2$/i, 'Apache-2.0') | ||
| .replace(/^MIT$/i, 'MIT') | ||
| .replace(/^ISC$/i, 'ISC') | ||
| .replace(/^BSD$/i, 'BSD-3-Clause') | ||
| .replace(/^GPLv3$/i, 'GPL-3.0') | ||
| .replace(/^GPLv2$/i, 'GPL-2.0') | ||
| .replace(/^LGPLv3$/i, 'LGPL-3.0') | ||
| .replace(/^LGPLv2\.1$/i, 'LGPL-2.1') | ||
| .replace(/^AGPLv3$/i, 'AGPL-3.0'); | ||
| return normalized; | ||
| } | ||
| const license = packageData.license || 'Unknown'; | ||
| const normalizedLicense = normalizeLicense(license); | ||
| const riskInfo = LICENSE_RISKS[normalizedLicense] || { | ||
| level: 'unknown', | ||
| risk: 'License terms unclear' | ||
| }; | ||
| function getLicenseRisk(license) { | ||
| const normalized = normalizeLicense(license); | ||
| if (LICENSE_RISKS[normalized]) { | ||
| return { | ||
| license: normalized, | ||
| ...LICENSE_RISKS[normalized] | ||
| package: packageName, | ||
| version: version, | ||
| license: normalizedLicense, | ||
| riskLevel: riskInfo.level, | ||
| risk: riskInfo.risk, | ||
| isGPL: normalizedLicense.includes('GPL'), | ||
| isPermissive: riskInfo.level === 'low', | ||
| alternative: GPL_ALTERNATIVES[packageName] || null | ||
| }; | ||
| } | ||
| // Check for partial matches | ||
| for (const [key, value] of Object.entries(LICENSE_RISKS)) { | ||
| if (normalized.toUpperCase().includes(key.toUpperCase())) { | ||
| return { license: normalized, ...value }; | ||
| } | ||
| } | ||
| // Unknown license | ||
| return { | ||
| license: normalized, | ||
| level: 'unknown', | ||
| risk: 'Unknown license - manual review recommended' | ||
| }; | ||
| } | ||
| async function analyzePackage(packageName) { | ||
| const result = { | ||
| name: packageName, | ||
| license: null, | ||
| riskLevel: 'unknown', | ||
| riskMessage: null, | ||
| hasIssue: false, | ||
| alternative: null, | ||
| source: 'live' | ||
| }; | ||
| try { | ||
| const data = await registryClient.fetchPackage(packageName); | ||
| if (!data) { | ||
| result.license = 'UNKNOWN'; | ||
| result.riskMessage = 'Package not found'; | ||
| return result; | ||
| } | ||
| // Get license from latest version | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| const license = latestData?.license || data.license; | ||
| if (!license) { | ||
| result.license = 'UNLICENSED'; | ||
| result.riskLevel = 'high'; | ||
| result.riskMessage = 'No license specified'; | ||
| result.hasIssue = true; | ||
| return result; | ||
| } | ||
| // Analyze the license | ||
| const riskInfo = getLicenseRisk(license); | ||
| result.license = riskInfo.license; | ||
| result.riskLevel = riskInfo.level; | ||
| result.riskMessage = riskInfo.risk; | ||
| result.hasIssue = ['critical', 'high', 'medium'].includes(riskInfo.level); | ||
| // Check for known alternatives | ||
| if (GPL_ALTERNATIVES[packageName]) { | ||
| result.alternative = GPL_ALTERNATIVES[packageName].replacement; | ||
| } | ||
| return result; | ||
| } catch (error) { | ||
| result.license = 'UNKNOWN'; | ||
| result.riskMessage = 'Failed to fetch license info'; | ||
| return result; | ||
| return null; | ||
| } | ||
| } | ||
| async function analyzeBatch(packageNames) { | ||
| if (!Array.isArray(packageNames)) { | ||
| return new Map(); | ||
| } | ||
| /** | ||
| * Analyze licenses for multiple packages | ||
| */ | ||
| async function analyzeBatch(packages) { | ||
| // FIXED: Handle both array and object inputs | ||
| const packageArray = Array.isArray(packages) | ||
| ? packages | ||
| : Object.entries(packages).map(([name, version]) => ({ name, version })); | ||
| const results = new Map(); | ||
| const packageData = await registryClient.fetchBatch(packageNames); | ||
| const results = []; | ||
| for (const name of packageNames) { | ||
| if (!name || typeof name !== 'string') continue; | ||
| const data = packageData.get(name); | ||
| const result = { | ||
| name, | ||
| license: null, | ||
| riskLevel: 'unknown', | ||
| riskMessage: null, | ||
| hasIssue: false, | ||
| alternative: GPL_ALTERNATIVES[name]?.replacement || null, | ||
| source: data ? 'live' : 'none' | ||
| }; | ||
| if (data) { | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| const license = latestData?.license || data.license; | ||
| if (!license) { | ||
| result.license = 'UNLICENSED'; | ||
| result.riskLevel = 'high'; | ||
| result.riskMessage = 'No license specified'; | ||
| result.hasIssue = true; | ||
| } else { | ||
| const riskInfo = getLicenseRisk(license); | ||
| result.license = riskInfo.license; | ||
| result.riskLevel = riskInfo.level; | ||
| result.riskMessage = riskInfo.risk; | ||
| result.hasIssue = ['critical', 'high', 'medium'].includes(riskInfo.level); | ||
| } | ||
| } else { | ||
| result.license = 'UNKNOWN'; | ||
| result.riskMessage = 'Package not found'; | ||
| for (const pkg of packageArray) { | ||
| const analysis = await analyzePackage(pkg.name, pkg.version); | ||
| if (analysis) { | ||
| results.push(analysis); | ||
| } | ||
| results.set(name, result); | ||
| } | ||
| return results; | ||
| } | ||
| async function getLicenseConflicts(packageNames, projectLicense = 'MIT') { | ||
| const results = await analyzeBatch(packageNames); | ||
| const conflicts = { | ||
| total: packageNames.length, | ||
| clean: 0, | ||
| critical: [], | ||
| high: [], | ||
| medium: [], | ||
| unknown: [] | ||
| }; | ||
| for (const [name, data] of results) { | ||
| switch (data.riskLevel) { | ||
| case 'critical': | ||
| conflicts.critical.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'high': | ||
| conflicts.high.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'medium': | ||
| conflicts.medium.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'unknown': | ||
| conflicts.unknown.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage | ||
| }); | ||
| break; | ||
| default: | ||
| conflicts.clean++; | ||
| /** | ||
| * Get license conflicts | ||
| */ | ||
| function getLicenseConflicts(analyses, projectLicense = 'MIT') { | ||
| const conflicts = []; | ||
| // FIXED: Ensure analyses is an array | ||
| const analysesArray = Array.isArray(analyses) ? analyses : []; | ||
| analysesArray.forEach(analysis => { | ||
| if (analysis.riskLevel === 'critical' || analysis.riskLevel === 'high') { | ||
| conflicts.push({ | ||
| package: analysis.package, | ||
| license: analysis.license, | ||
| projectLicense: projectLicense, | ||
| severity: analysis.riskLevel, | ||
| reason: analysis.risk, | ||
| autoFixable: !!analysis.alternative, | ||
| suggestedAlternative: analysis.alternative?.replacement | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
| return conflicts; | ||
| } | ||
| /** | ||
| * Get license risk level | ||
| */ | ||
| function getLicenseRisk(license) { | ||
| const normalizedLicense = normalizeLicense(license); | ||
| const riskInfo = LICENSE_RISKS[normalizedLicense]; | ||
| return riskInfo ? riskInfo.level : 'unknown'; | ||
| } | ||
| /** | ||
| * Check if license is commercial-compatible | ||
| */ | ||
| function isCommercialCompatible(license) { | ||
| const risk = getLicenseRisk(license); | ||
| return risk.level === 'low'; | ||
| return risk === 'low' || risk === 'unknown'; | ||
| } | ||
| /** | ||
| * Normalize license string | ||
| */ | ||
| function normalizeLicense(license) { | ||
| if (!license || typeof license !== 'string') { | ||
| return 'Unknown'; | ||
| } | ||
| // Handle SPDX expressions | ||
| if (license.includes('OR') || license.includes('AND')) { | ||
| const licenses = license.split(/\s+(OR|AND)\s+/); | ||
| return licenses[0].trim(); | ||
| } | ||
| return license.trim(); | ||
| } | ||
| module.exports = { | ||
@@ -262,0 +131,0 @@ analyzePackage, |
| // src/services/dynamic-quality.js | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
| const registryClient = require('./registry-client'); | ||
| // Minimal fallback alternatives for offline mode | ||
| // Only most common replacements - live data covers everything else | ||
| const FALLBACK_ALTERNATIVES = { | ||
| 'request': { replacement: 'axios', reason: 'request is deprecated' }, | ||
| 'moment': { replacement: 'dayjs', reason: 'moment is in maintenance mode' }, | ||
| 'underscore': { replacement: 'lodash', reason: 'lodash is more actively maintained' }, | ||
| 'colors': { replacement: 'chalk', reason: 'colors had a malicious release' }, | ||
| 'faker': { replacement: '@faker-js/faker', reason: 'faker was corrupted, use community fork' }, | ||
| 'left-pad': { replacement: 'string.prototype.padstart', reason: 'Use native String methods' }, | ||
| 'node-uuid': { replacement: 'uuid', reason: 'node-uuid is deprecated' }, | ||
| 'querystring': { replacement: 'qs', reason: 'querystring is legacy' }, | ||
| 'node-fetch': { replacement: 'undici', reason: 'undici is faster and maintained by Node.js' } | ||
| }; | ||
| // Load quality alternatives from JSON | ||
| const FALLBACK_ALTERNATIVES = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/quality-alternatives.json'), 'utf8') | ||
| ); | ||
| // Thresholds for maintenance status | ||
| const ABANDONED_THRESHOLD_MONTHS = 36; | ||
| const STALE_THRESHOLD_MONTHS = 24; | ||
| // Thresholds (in milliseconds) | ||
| const ABANDONED_THRESHOLD = 36 * 30 * 24 * 60 * 60 * 1000; // 36 months | ||
| const STALE_THRESHOLD = 24 * 30 * 24 * 60 * 60 * 1000; // 24 months | ||
| function monthsSince(date) { | ||
| if (!date) return Infinity; | ||
| const then = new Date(date); | ||
| const now = new Date(); | ||
| return Math.floor((now - then) / (1000 * 60 * 60 * 24 * 30)); | ||
| } | ||
| async function analyzePackage(packageName) { | ||
| const result = { | ||
| name: packageName, | ||
| status: 'HEALTHY', | ||
| deprecated: false, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: null, | ||
| alternative: null, | ||
| source: 'live' // 'live' or 'fallback' | ||
| }; | ||
| /** | ||
| * Analyze a package's quality | ||
| */ | ||
| async function analyzePackage(packageName, version) { | ||
| try { | ||
| const data = await registryClient.fetchPackage(packageName); | ||
| const packageData = await registryClient.getPackageData(packageName, version); | ||
| if (!data) { | ||
| // Package not found - check fallback | ||
| if (FALLBACK_ALTERNATIVES[packageName]) { | ||
| const fallback = FALLBACK_ALTERNATIVES[packageName]; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecated = true; | ||
| result.alternative = fallback.replacement; | ||
| result.deprecationMessage = fallback.reason; | ||
| result.source = 'fallback'; | ||
| } | ||
| return result; | ||
| if (!packageData) { | ||
| return null; | ||
| } | ||
| // Check deprecation status from registry | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| if (latestData?.deprecated || data.deprecated) { | ||
| result.deprecated = true; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecationMessage = latestData?.deprecated || data.deprecated || 'Package is deprecated'; | ||
| const time = packageData.time || {}; | ||
| const modified = time.modified || time[version] || Date.now(); | ||
| const lastPublish = new Date(modified); | ||
| const now = new Date(); | ||
| const ageMs = now - lastPublish; | ||
| const deprecated = packageData.deprecated || false; | ||
| const isAbandoned = ageMs > ABANDONED_THRESHOLD; | ||
| const isStale = ageMs > STALE_THRESHOLD && ageMs <= ABANDONED_THRESHOLD; | ||
| let status = 'healthy'; | ||
| let healthScore = 10; | ||
| if (deprecated) { | ||
| status = 'deprecated'; | ||
| healthScore = 0; | ||
| } else if (isAbandoned) { | ||
| status = 'abandoned'; | ||
| healthScore = 2; | ||
| } else if (isStale) { | ||
| status = 'stale'; | ||
| healthScore = 5; | ||
| } else { | ||
| status = 'healthy'; | ||
| healthScore = 10; | ||
| } | ||
| // Check maintenance status | ||
| const timeData = data.time || {}; | ||
| const lastModified = timeData.modified || timeData[latestVersion]; | ||
| if (lastModified) { | ||
| result.lastPublish = lastModified; | ||
| result.monthsSinceUpdate = monthsSince(lastModified); | ||
| if (result.monthsSinceUpdate >= ABANDONED_THRESHOLD_MONTHS) { | ||
| result.abandoned = true; | ||
| if (result.status === 'HEALTHY') { | ||
| result.status = 'ABANDONED'; | ||
| } | ||
| } else if (result.monthsSinceUpdate >= STALE_THRESHOLD_MONTHS) { | ||
| result.stale = true; | ||
| if (result.status === 'HEALTHY') { | ||
| result.status = 'STALE'; | ||
| } | ||
| } | ||
| } | ||
| // Check fallback for known alternatives | ||
| if (FALLBACK_ALTERNATIVES[packageName]) { | ||
| result.alternative = FALLBACK_ALTERNATIVES[packageName].replacement; | ||
| } | ||
| return result; | ||
| const alternative = getAlternative(packageName); | ||
| return { | ||
| package: packageName, | ||
| version: version, | ||
| status: status, | ||
| healthScore: healthScore, | ||
| lastUpdate: lastPublish.toISOString(), | ||
| ageMonths: Math.floor(ageMs / (30 * 24 * 60 * 60 * 1000)), | ||
| deprecated: deprecated, | ||
| alternative: alternative, | ||
| autoFixable: (deprecated || isAbandoned) && !!alternative | ||
| }; | ||
| } catch (error) { | ||
| // Network error - check fallback | ||
| if (FALLBACK_ALTERNATIVES[packageName]) { | ||
| const fallback = FALLBACK_ALTERNATIVES[packageName]; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecated = true; | ||
| result.alternative = fallback.replacement; | ||
| result.deprecationMessage = fallback.reason; | ||
| result.source = 'fallback'; | ||
| } | ||
| return result; | ||
| return null; | ||
| } | ||
| } | ||
| async function analyzeBatch(packageNames) { | ||
| if (!Array.isArray(packageNames)) { | ||
| return new Map(); | ||
| } | ||
| /** | ||
| * Analyze multiple packages | ||
| */ | ||
| async function analyzeBatch(packages) { | ||
| const results = []; | ||
| const results = new Map(); | ||
| // Fetch all packages in parallel | ||
| const packageData = await registryClient.fetchBatch(packageNames); | ||
| // Analyze each package | ||
| for (const name of packageNames) { | ||
| if (!name || typeof name !== 'string') continue; | ||
| const data = packageData.get(name); | ||
| if (data) { | ||
| // We have live data | ||
| const result = { | ||
| name, | ||
| status: 'HEALTHY', | ||
| deprecated: false, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: null, | ||
| alternative: FALLBACK_ALTERNATIVES[name]?.replacement || null, | ||
| source: 'live' | ||
| }; | ||
| // Check deprecation | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| if (latestData?.deprecated || data.deprecated) { | ||
| result.deprecated = true; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecationMessage = latestData?.deprecated || data.deprecated || 'Package is deprecated'; | ||
| } | ||
| // Check maintenance | ||
| const timeData = data.time || {}; | ||
| const lastModified = timeData.modified || timeData[latestVersion]; | ||
| if (lastModified) { | ||
| result.lastPublish = lastModified; | ||
| result.monthsSinceUpdate = monthsSince(lastModified); | ||
| if (result.monthsSinceUpdate >= ABANDONED_THRESHOLD_MONTHS) { | ||
| result.abandoned = true; | ||
| if (result.status === 'HEALTHY') result.status = 'ABANDONED'; | ||
| } else if (result.monthsSinceUpdate >= STALE_THRESHOLD_MONTHS) { | ||
| result.stale = true; | ||
| if (result.status === 'HEALTHY') result.status = 'STALE'; | ||
| } | ||
| } | ||
| results.set(name, result); | ||
| } else { | ||
| // Check fallback | ||
| if (FALLBACK_ALTERNATIVES[name]) { | ||
| const fallback = FALLBACK_ALTERNATIVES[name]; | ||
| results.set(name, { | ||
| name, | ||
| status: 'DEPRECATED', | ||
| deprecated: true, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: fallback.reason, | ||
| alternative: fallback.replacement, | ||
| source: 'fallback' | ||
| }); | ||
| } else { | ||
| // Unknown package | ||
| results.set(name, { | ||
| name, | ||
| status: 'UNKNOWN', | ||
| deprecated: false, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: null, | ||
| alternative: null, | ||
| source: 'none' | ||
| }); | ||
| } | ||
| for (const pkg of packages) { | ||
| const analysis = await analyzePackage(pkg.name, pkg.version); | ||
| if (analysis) { | ||
| results.push(analysis); | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| async function getProjectQualitySummary(packageNames) { | ||
| const results = await analyzeBatch(packageNames); | ||
| /** | ||
| * Get project quality summary | ||
| */ | ||
| function getProjectQualitySummary(analyses) { | ||
| const summary = { | ||
| total: packageNames.length, | ||
| total: analyses.length, | ||
| healthy: 0, | ||
| stale: 0, | ||
| abandoned: 0, | ||
| deprecated: 0, | ||
| abandoned: 0, | ||
| stale: 0, | ||
| unknown: 0, | ||
| issues: [] | ||
| averageHealth: 0 | ||
| }; | ||
| for (const [name, data] of results) { | ||
| switch (data.status) { | ||
| case 'HEALTHY': | ||
| let totalHealth = 0; | ||
| analyses.forEach(analysis => { | ||
| totalHealth += analysis.healthScore; | ||
| switch (analysis.status) { | ||
| case 'healthy': | ||
| summary.healthy++; | ||
| break; | ||
| case 'DEPRECATED': | ||
| summary.deprecated++; | ||
| summary.issues.push({ | ||
| package: name, | ||
| type: 'deprecated', | ||
| message: data.deprecationMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| case 'stale': | ||
| summary.stale++; | ||
| break; | ||
| case 'ABANDONED': | ||
| case 'abandoned': | ||
| summary.abandoned++; | ||
| summary.issues.push({ | ||
| package: name, | ||
| type: 'abandoned', | ||
| message: `No updates in ${data.monthsSinceUpdate} months`, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'STALE': | ||
| summary.stale++; | ||
| summary.issues.push({ | ||
| package: name, | ||
| type: 'stale', | ||
| message: `No updates in ${data.monthsSinceUpdate} months`, | ||
| alternative: data.alternative | ||
| }); | ||
| case 'deprecated': | ||
| summary.deprecated++; | ||
| break; | ||
| default: | ||
| summary.unknown++; | ||
| } | ||
| } | ||
| }); | ||
| summary.averageHealth = analyses.length > 0 | ||
| ? (totalHealth / analyses.length).toFixed(1) | ||
| : 0; | ||
| return summary; | ||
| } | ||
| /** | ||
| * Get alternative for a package | ||
| */ | ||
| function getAlternative(packageName) { | ||
| return FALLBACK_ALTERNATIVES[packageName]?.replacement || null; | ||
| return FALLBACK_ALTERNATIVES[packageName] || null; | ||
| } | ||
@@ -264,0 +134,0 @@ |
+176
-228
| // src/services/dynamic-security.js | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
| const { execSync } = require('child_process'); | ||
| const path = require('path'); | ||
| // Popular packages for typosquatting comparison | ||
| const POPULAR_PACKAGES = [ | ||
| 'express', 'react', 'react-dom', 'lodash', 'axios', 'moment', 'webpack', | ||
| 'typescript', 'jquery', 'vue', 'angular', 'next', 'gatsby', 'nuxt', | ||
| 'babel', 'eslint', 'prettier', 'jest', 'mocha', 'chai', 'karma', | ||
| 'underscore', 'request', 'async', 'bluebird', 'chalk', 'commander', | ||
| 'inquirer', 'ora', 'yargs', 'minimist', 'glob', 'mkdirp', 'rimraf', | ||
| 'fs-extra', 'uuid', 'semver', 'debug', 'dotenv', 'cors', 'helmet', | ||
| 'mongoose', 'sequelize', 'knex', 'prisma', 'graphql', 'apollo', | ||
| 'socket.io', 'redis', 'mysql', 'pg', 'sqlite3', 'mongodb', | ||
| 'aws-sdk', 'firebase', 'stripe', 'paypal', 'twilio', | ||
| 'nodemailer', 'pug', 'ejs', 'handlebars', 'mustache', | ||
| 'body-parser', 'cookie-parser', 'multer', 'passport', 'bcrypt', | ||
| 'jsonwebtoken', 'validator', 'yup', 'joi', 'zod' | ||
| ]; | ||
| // Load security data from JSON | ||
| const securityData = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/popular-packages.json'), 'utf8') | ||
| ); | ||
| // Whitelist - legitimate packages that might look suspicious | ||
| const WHITELIST = new Set([ | ||
| 'colors', 'chalk', 'ora', 'inquirer', 'prompts', | ||
| 'cross-env', 'cross-spawn', 'execa', 'shelljs', | ||
| 'node-fetch', 'axios', 'got', 'request', 'superagent', | ||
| 'nodemailer', 'sendgrid', 'mailgun', | ||
| 'bcrypt', 'bcryptjs', 'argon2', 'crypto-js', | ||
| 'jsonwebtoken', 'jose', 'passport', | ||
| 'puppeteer', 'playwright', 'selenium-webdriver', | ||
| 'sharp', 'jimp', 'canvas', 'pdf-lib', | ||
| 'esbuild', 'rollup', 'vite', 'parcel', | ||
| 'husky', 'lint-staged', 'commitlint' | ||
| ]); | ||
| const POPULAR_PACKAGES = securityData.packages; | ||
| const WHITELIST = new Set(securityData.whitelist); | ||
| // Suspicious patterns in install scripts | ||
| const SUSPICIOUS_PATTERNS = [ | ||
| /curl\s+[^\|]+\|\s*sh/i, // curl | sh | ||
| /wget\s+[^\|]+\|\s*sh/i, // wget | sh | ||
| /eval\s*\(\s*["']?http/i, // eval with http | ||
| /base64\s*-d/i, // base64 decode | ||
| /\\x[0-9a-f]{2}/i, // hex escapes | ||
| /child_process.*exec/i, // exec with http | ||
| /require\s*\(\s*['"]https?:/i, // require http | ||
| /\.env|process\.env\.(AWS|STRIPE|API_KEY|SECRET|PASSWORD|TOKEN)/i, // credential access | ||
| /exfiltrate|steal|harvest/i, // obvious malicious | ||
| /bitcoin|btc|wallet|miner/i, // crypto mining | ||
| /keylog|screenshot|record/i // spyware | ||
| ]; | ||
| function levenshteinDistance(a, b) { | ||
| if (!a || !b) return Infinity; | ||
| const matrix = Array(b.length + 1).fill(null).map(() => | ||
| Array(a.length + 1).fill(null) | ||
| ); | ||
| for (let i = 0; i <= a.length; i++) matrix[0][i] = i; | ||
| for (let j = 0; j <= b.length; j++) matrix[j][0] = j; | ||
| for (let j = 1; j <= b.length; j++) { | ||
| for (let i = 1; i <= a.length; i++) { | ||
| const cost = a[i - 1] === b[j - 1] ? 0 : 1; | ||
| matrix[j][i] = Math.min( | ||
| matrix[j][i - 1] + 1, // insertion | ||
| matrix[j - 1][i] + 1, // deletion | ||
| matrix[j - 1][i - 1] + cost // substitution | ||
| ); | ||
| } | ||
| /** | ||
| * Check for typosquatting attempts | ||
| */ | ||
| function checkTyposquatting(packageName) { | ||
| if (WHITELIST.has(packageName)) { | ||
| return null; | ||
| } | ||
| return matrix[b.length][a.length]; | ||
| } | ||
| function checkTyposquatting(packageName, threshold = 2) { | ||
| if (!packageName || WHITELIST.has(packageName)) return null; | ||
| const name = packageName.toLowerCase().replace(/^@[^/]+\//, ''); // Remove scope | ||
| for (const popular of POPULAR_PACKAGES) { | ||
| const distance = levenshteinDistance(name, popular); | ||
| for (const official of POPULAR_PACKAGES) { | ||
| const distance = levenshteinDistance(packageName, official); | ||
| // Exact match is fine | ||
| if (distance === 0) return null; | ||
| // Within threshold and not too short | ||
| if (distance <= threshold && name.length >= 3 && popular.length >= 3) { | ||
| // Additional heuristics | ||
| const isSuspicious = ( | ||
| name.includes(popular) || // lodash-extra | ||
| popular.includes(name) || // lod (subset) | ||
| name.replace(/[-_]/g, '') === popular.replace(/[-_]/g, '') || // lodash_js vs lodashjs | ||
| name.startsWith(popular.slice(0, 3)) || // Same prefix | ||
| name.endsWith(popular.slice(-3)) // Same suffix | ||
| ); | ||
| if (isSuspicious || distance === 1) { | ||
| return { | ||
| package: packageName, | ||
| similarTo: popular, | ||
| distance, | ||
| warning: `Package name "${packageName}" is similar to popular package "${popular}"` | ||
| }; | ||
| } | ||
| if (distance > 0 && distance <= 2) { | ||
| return { | ||
| package: packageName, | ||
| official: official, | ||
| distance: distance, | ||
| type: 'typosquat', | ||
| severity: 'high' | ||
| }; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Check for suspicious install scripts | ||
| */ | ||
| function checkInstallScripts(packageJsonPath) { | ||
| try { | ||
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); | ||
| const scripts = packageJson.scripts || {}; | ||
| const suspiciousPatterns = [ | ||
| /curl.*\|.*sh/i, | ||
| /wget.*\|.*sh/i, | ||
| /eval.*\(/i, | ||
| /exec.*\(/i, | ||
| /child_process/i, | ||
| /\.download/i, | ||
| /http:\/\//i, | ||
| /https:\/\//i, | ||
| /bitcoin/i, | ||
| /mining/i, | ||
| /keylogger/i | ||
| ]; | ||
| function checkInstallScripts(packageData) { | ||
| if (!packageData) return null; | ||
| const scripts = packageData.scripts || {}; | ||
| const installScripts = ['preinstall', 'install', 'postinstall']; | ||
| const suspicious = []; | ||
| for (const scriptName of installScripts) { | ||
| const script = scripts[scriptName]; | ||
| if (!script) continue; | ||
| for (const pattern of SUSPICIOUS_PATTERNS) { | ||
| if (pattern.test(script)) { | ||
| suspicious.push({ | ||
| script: scriptName, | ||
| command: script.slice(0, 100) + (script.length > 100 ? '...' : ''), | ||
| pattern: pattern.source | ||
| }); | ||
| break; // One match per script is enough | ||
| const suspiciousScripts = []; | ||
| for (const [scriptName, scriptContent] of Object.entries(scripts)) { | ||
| if (['postinstall', 'preinstall', 'install'].includes(scriptName)) { | ||
| for (const pattern of suspiciousPatterns) { | ||
| if (pattern.test(scriptContent)) { | ||
| suspiciousScripts.push({ | ||
| script: scriptName, | ||
| content: scriptContent, | ||
| pattern: pattern.toString(), | ||
| severity: 'medium' | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return suspiciousScripts; | ||
| } catch (error) { | ||
| return []; | ||
| } | ||
| return suspicious.length > 0 ? { | ||
| package: packageData.name, | ||
| suspicious, | ||
| warning: `Package has suspicious install scripts` | ||
| } : null; | ||
| } | ||
| function runNpmAudit(projectPath) { | ||
| /** | ||
| * Run npm audit - FIXED to always return array | ||
| */ | ||
| async function runNpmAudit(projectPath) { | ||
| try { | ||
| // Run npm audit in JSON mode | ||
| const result = execSync('npm audit --json 2>/dev/null', { | ||
| const output = execSync('npm audit --json', { | ||
| cwd: projectPath, | ||
| encoding: 'utf8', | ||
| maxBuffer: 10 * 1024 * 1024 // 10MB | ||
| encoding: 'utf-8', | ||
| stdio: ['pipe', 'pipe', 'ignore'] | ||
| }); | ||
| const audit = JSON.parse(result); | ||
| return parseAuditResult(audit); | ||
| const auditData = JSON.parse(output); | ||
| return parseAuditData(auditData); | ||
| } catch (error) { | ||
| // npm audit exits with non-zero if vulnerabilities found | ||
| if (error.stdout) { | ||
| try { | ||
| const audit = JSON.parse(error.stdout); | ||
| return parseAuditResult(audit); | ||
| } catch (e) { | ||
| // Silent fail | ||
| const auditData = JSON.parse(error.stdout); | ||
| return parseAuditData(auditData); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| return { | ||
| vulnerabilities: [], | ||
| summary: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 } | ||
| }; | ||
| return []; | ||
| } | ||
| } | ||
| function parseAuditResult(audit) { | ||
| /** | ||
| * Parse audit data - FIXED to always return array | ||
| */ | ||
| function parseAuditData(auditData) { | ||
| const vulnerabilities = []; | ||
| const summary = { | ||
| total: 0, | ||
| critical: 0, | ||
| high: 0, | ||
| moderate: 0, | ||
| low: 0, | ||
| info: 0 | ||
| }; | ||
| // Handle npm v7+ format | ||
| if (audit.vulnerabilities) { | ||
| for (const [name, data] of Object.entries(audit.vulnerabilities)) { | ||
| if (!data.via || !Array.isArray(data.via)) continue; | ||
| for (const via of data.via) { | ||
| if (typeof via === 'object' && via.title) { | ||
| vulnerabilities.push({ | ||
| package: name, | ||
| severity: via.severity || 'unknown', | ||
| title: via.title, | ||
| url: via.url, | ||
| range: via.range || data.range | ||
| }); | ||
| const sev = (via.severity || 'low').toLowerCase(); | ||
| if (summary[sev] !== undefined) summary[sev]++; | ||
| summary.total++; | ||
| } | ||
| try { | ||
| // npm v7+ format | ||
| if (auditData.vulnerabilities && typeof auditData.vulnerabilities === 'object') { | ||
| for (const [name, vuln] of Object.entries(auditData.vulnerabilities)) { | ||
| vulnerabilities.push({ | ||
| package: name, | ||
| severity: vuln.severity || 'unknown', | ||
| via: Array.isArray(vuln.via) ? vuln.via : [vuln.via], | ||
| fixAvailable: vuln.fixAvailable || false | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| // Handle npm v6 format | ||
| if (audit.advisories) { | ||
| for (const [id, advisory] of Object.entries(audit.advisories)) { | ||
| vulnerabilities.push({ | ||
| package: advisory.module_name, | ||
| severity: advisory.severity, | ||
| title: advisory.title, | ||
| url: advisory.url, | ||
| range: advisory.vulnerable_versions | ||
| }); | ||
| const sev = (advisory.severity || 'low').toLowerCase(); | ||
| if (summary[sev] !== undefined) summary[sev]++; | ||
| summary.total++; | ||
| // npm v6 format | ||
| else if (auditData.advisories && typeof auditData.advisories === 'object') { | ||
| for (const advisory of Object.values(auditData.advisories)) { | ||
| vulnerabilities.push({ | ||
| package: advisory.module_name || 'unknown', | ||
| severity: advisory.severity || 'unknown', | ||
| via: [advisory.title || 'Unknown vulnerability'], | ||
| fixAvailable: true | ||
| }); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error('Error parsing audit data:', error.message); | ||
| } | ||
| return { vulnerabilities, summary }; | ||
| // ALWAYS return array, never undefined/null | ||
| return vulnerabilities; | ||
| } | ||
| async function analyzeProject(projectPath, dependencies = []) { | ||
| const result = { | ||
| /** | ||
| * Analyze entire project for security issues - FIXED to always return proper structure | ||
| */ | ||
| async function analyzeProject(projectPath) { | ||
| const results = { | ||
| typosquatting: [], | ||
| vulnerabilities: [], | ||
| suspiciousScripts: [], | ||
| summary: { | ||
| typosquattingCount: 0, | ||
| vulnerabilityCount: 0, | ||
| suspiciousScriptCount: 0, | ||
| criticalCount: 0, | ||
| highCount: 0 | ||
| } | ||
| vulnerabilities: [] // Always an array | ||
| }; | ||
| // Check for typosquatting | ||
| for (const dep of dependencies) { | ||
| if (!dep || WHITELIST.has(dep)) continue; | ||
| try { | ||
| const packageJsonPath = path.join(projectPath, 'package.json'); | ||
| const typosquat = checkTyposquatting(dep); | ||
| if (typosquat) { | ||
| result.typosquatting.push(typosquat); | ||
| result.summary.typosquattingCount++; | ||
| if (!fs.existsSync(packageJsonPath)) { | ||
| return results; | ||
| } | ||
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); | ||
| const dependencies = { | ||
| ...packageJson.dependencies, | ||
| ...packageJson.devDependencies | ||
| }; | ||
| // Check for typosquatting | ||
| for (const dep of Object.keys(dependencies)) { | ||
| const typosquat = checkTyposquatting(dep); | ||
| if (typosquat) { | ||
| results.typosquatting.push(typosquat); | ||
| } | ||
| } | ||
| // Check for suspicious install scripts | ||
| results.suspiciousScripts = checkInstallScripts(packageJsonPath); | ||
| // Run npm audit - ALWAYS returns array | ||
| results.vulnerabilities = await runNpmAudit(projectPath); | ||
| } catch (error) { | ||
| console.error('Security analysis failed:', error.message); | ||
| } | ||
| // Run npm audit | ||
| if (projectPath) { | ||
| const audit = runNpmAudit(projectPath); | ||
| result.vulnerabilities = audit.vulnerabilities; | ||
| result.summary.vulnerabilityCount = audit.summary.total; | ||
| result.summary.criticalCount = audit.summary.critical; | ||
| result.summary.highCount = audit.summary.high; | ||
| } | ||
| return result; | ||
| } | ||
| function addToWhitelist(packageName) { | ||
| WHITELIST.add(packageName); | ||
| // Ensure all fields are arrays | ||
| return { | ||
| typosquatting: results.typosquatting || [], | ||
| suspiciousScripts: results.suspiciousScripts || [], | ||
| vulnerabilities: results.vulnerabilities || [] | ||
| }; | ||
| } | ||
| function removeFromWhitelist(packageName) { | ||
| WHITELIST.delete(packageName); | ||
| } | ||
| /** | ||
| * Levenshtein distance algorithm | ||
| */ | ||
| function levenshteinDistance(str1, str2) { | ||
| const matrix = []; | ||
| for (let i = 0; i <= str2.length; i++) { | ||
| matrix[i] = [i]; | ||
| } | ||
| function isWhitelisted(packageName) { | ||
| return WHITELIST.has(packageName); | ||
| for (let j = 0; j <= str1.length; j++) { | ||
| matrix[0][j] = j; | ||
| } | ||
| for (let i = 1; i <= str2.length; i++) { | ||
| for (let j = 1; j <= str1.length; j++) { | ||
| if (str2.charAt(i - 1) === str1.charAt(j - 1)) { | ||
| matrix[i][j] = matrix[i - 1][j - 1]; | ||
| } else { | ||
| matrix[i][j] = Math.min( | ||
| matrix[i - 1][j - 1] + 1, | ||
| matrix[i][j - 1] + 1, | ||
| matrix[i - 1][j] + 1 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| return matrix[str2.length][str1.length]; | ||
| } | ||
@@ -283,7 +233,5 @@ | ||
| analyzeProject, | ||
| addToWhitelist, | ||
| removeFromWhitelist, | ||
| isWhitelisted, | ||
| parseAuditData, // Export for testing | ||
| POPULAR_PACKAGES, | ||
| WHITELIST | ||
| }; |
+96
-142
| // src/utils/batch-selector.js | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
| const readline = require('readline'); | ||
| const chalk = require('chalk'); | ||
| // Load batch categories from JSON | ||
| const batchCategories = JSON.parse( | ||
| fs.readFileSync(path.join(__dirname, '../../data/batch-categories.json'), 'utf8') | ||
| ); | ||
| class BatchSelector { | ||
| constructor() { | ||
| this.batches = [ | ||
| { | ||
| id: 'supply-chain', | ||
| name: 'Supply Chain Security', | ||
| icon: '๐ก๏ธ', | ||
| priority: 1, | ||
| description: 'Malicious packages, typosquatting, suspicious scripts' | ||
| }, | ||
| { | ||
| id: 'license', | ||
| name: 'License Conflicts', | ||
| icon: 'โ๏ธ', | ||
| priority: 2, | ||
| description: 'GPL/AGPL/LGPL package replacements' | ||
| }, | ||
| { | ||
| id: 'quality', | ||
| name: 'Package Quality', | ||
| icon: '๐ฆ', | ||
| priority: 3, | ||
| description: 'Abandoned, deprecated, stale packages' | ||
| }, | ||
| { | ||
| id: 'security', | ||
| name: 'Critical Security', | ||
| icon: '๐', | ||
| priority: 4, | ||
| description: 'npm audit vulnerabilities' | ||
| }, | ||
| { | ||
| id: 'ecosystem', | ||
| name: 'Ecosystem Alerts', | ||
| icon: '๐จ', | ||
| priority: 5, | ||
| description: 'Known package issues' | ||
| }, | ||
| { | ||
| id: 'unused', | ||
| name: 'Unused Dependencies', | ||
| icon: '๐งน', | ||
| priority: 6, | ||
| description: 'Remove unused packages' | ||
| }, | ||
| { | ||
| id: 'updates', | ||
| name: 'Safe Updates', | ||
| icon: 'โฌ๏ธ', | ||
| priority: 7, | ||
| description: 'Patch and minor version updates' | ||
| } | ||
| ]; | ||
| this.batches = batchCategories; | ||
| } | ||
| /** | ||
| * Get batch statistics from planned fixes | ||
| * Get fix statistics for each batch | ||
| */ | ||
| getBatchStats(plannedFixes) { | ||
| getBatchStats(analysisResults) { | ||
| const stats = {}; | ||
| this.batches.forEach(batch => { | ||
| stats[batch.id] = { | ||
| count: 0, | ||
| fixes: [] | ||
| }; | ||
| stats[batch.id] = 0; | ||
| }); | ||
| // Count supply chain fixes | ||
| if (plannedFixes.supplyChain?.length > 0) { | ||
| stats['supply-chain'].count = plannedFixes.supplyChain.length; | ||
| stats['supply-chain'].fixes = plannedFixes.supplyChain; | ||
| // Count fixes per category | ||
| const { | ||
| supplyChain, | ||
| licenseRisk, | ||
| quality, | ||
| security, | ||
| ecosystem, | ||
| unused, | ||
| outdated | ||
| } = analysisResults; | ||
| // Supply chain | ||
| if (supplyChain && supplyChain.warnings) { | ||
| stats['supply-chain'] = supplyChain.warnings.length; | ||
| } | ||
| // Count license fixes | ||
| if (plannedFixes.licenseConflicts?.length > 0) { | ||
| stats['license'].count = plannedFixes.licenseConflicts.length; | ||
| stats['license'].fixes = plannedFixes.licenseConflicts; | ||
| // License | ||
| if (licenseRisk && licenseRisk.warnings) { | ||
| stats['license'] = licenseRisk.warnings.filter(w => w.autoFixable).length; | ||
| } | ||
| // Count quality fixes | ||
| if (plannedFixes.quality?.length > 0) { | ||
| stats['quality'].count = plannedFixes.quality.length; | ||
| stats['quality'].fixes = plannedFixes.quality; | ||
| // Quality | ||
| if (quality && quality.packages) { | ||
| stats['quality'] = quality.packages.filter(p => | ||
| p.status === 'deprecated' || p.status === 'abandoned' | ||
| ).length; | ||
| } | ||
| // Count security fixes | ||
| if (plannedFixes.security?.criticalCount > 0) { | ||
| stats['security'].count = plannedFixes.security.criticalCount; | ||
| stats['security'].fixes = ['npm audit fix']; | ||
| // Security | ||
| if (security && security.vulnerabilities) { | ||
| stats['security'] = security.vulnerabilities.length > 0 ? 1 : 0; | ||
| } | ||
| // Count ecosystem fixes | ||
| if (plannedFixes.ecosystem?.length > 0) { | ||
| stats['ecosystem'].count = plannedFixes.ecosystem.length; | ||
| stats['ecosystem'].fixes = plannedFixes.ecosystem; | ||
| // Ecosystem | ||
| if (ecosystem && ecosystem.alerts) { | ||
| stats['ecosystem'] = ecosystem.alerts.length; | ||
| } | ||
| // Count unused dependencies | ||
| if (plannedFixes.unused?.length > 0) { | ||
| stats['unused'].count = plannedFixes.unused.length; | ||
| stats['unused'].fixes = plannedFixes.unused; | ||
| // Unused | ||
| if (unused && Array.isArray(unused)) { | ||
| stats['unused'] = unused.length > 0 ? 1 : 0; | ||
| } | ||
| // Count safe updates | ||
| if (plannedFixes.updates?.safe?.length > 0) { | ||
| stats['updates'].count = plannedFixes.updates.safe.length; | ||
| stats['updates'].fixes = plannedFixes.updates.safe; | ||
| // Updates | ||
| if (outdated && Array.isArray(outdated)) { | ||
| const safeUpdates = outdated.filter(p => | ||
| p.updateType === 'patch' || p.updateType === 'minor' | ||
| ); | ||
| stats['updates'] = safeUpdates.length > 0 ? 1 : 0; | ||
| } | ||
@@ -119,24 +82,33 @@ | ||
| /** | ||
| * Display batch selection menu | ||
| * Display batch menu | ||
| */ | ||
| displayBatchMenu(stats) { | ||
| console.log('\n' + chalk.bold.cyan('๐ฆ BATCH FIX MODE')); | ||
| console.log('\n' + chalk.cyan.bold('๐ฆ BATCH FIX MODE')); | ||
| console.log(chalk.gray('โ'.repeat(70))); | ||
| console.log('\nSelect which categories to fix:\n'); | ||
| console.log(chalk.white('Select which categories to fix:\n')); | ||
| this.batches.forEach((batch, index) => { | ||
| const count = stats[batch.id]?.count || 0; | ||
| const status = count > 0 ? chalk.yellow(`${count} fix(es)`) : chalk.gray('none'); | ||
| const count = stats[batch.id] || 0; | ||
| const hasFixed = count > 0; | ||
| console.log(`${chalk.bold(index + 1)}. ${batch.icon} ${chalk.bold(batch.name)}`); | ||
| console.log(` ${chalk.gray(batch.description)}`); | ||
| console.log(` Fixes available: ${status}\n`); | ||
| console.log(chalk.white(`${batch.icon} ${batch.name}`)); | ||
| console.log(chalk.gray(batch.description)); | ||
| console.log( | ||
| hasFixed | ||
| ? chalk.green(`Fixes available: ${count} fix(es)`) | ||
| : chalk.gray('Fixes available: none') | ||
| ); | ||
| if (index < this.batches.length - 1) { | ||
| console.log(''); | ||
| } | ||
| }); | ||
| console.log(chalk.gray('โ'.repeat(70))); | ||
| console.log('\n' + chalk.bold('Preset Modes:')); | ||
| console.log(`${chalk.bold('c')} - ${chalk.red('Critical only')} (supply-chain + license + security)`); | ||
| console.log(`${chalk.bold('h')} - ${chalk.yellow('High priority')} (critical + quality + ecosystem)`); | ||
| console.log(`${chalk.bold('a')} - ${chalk.green('All safe fixes')} (everything except major updates)`); | ||
| console.log(`${chalk.bold('n')} - ${chalk.gray('None')} (cancel)\n`); | ||
| console.log('\n' + chalk.gray('โ'.repeat(70))); | ||
| console.log(chalk.white('Preset Modes:')); | ||
| console.log(chalk.cyan('c') + ' - Critical only (supply-chain + license + security)'); | ||
| console.log(chalk.cyan('h') + ' - High priority (critical + quality + ecosystem)'); | ||
| console.log(chalk.cyan('a') + ' - All safe fixes (everything except major updates)'); | ||
| console.log(chalk.cyan('n') + ' - None (cancel)'); | ||
| console.log(chalk.gray('Enter your choice (1-7, c/h/a/n, or comma-separated):')); | ||
| } | ||
@@ -156,7 +128,6 @@ | ||
| return new Promise((resolve) => { | ||
| rl.question(chalk.cyan('Enter your choice (1-7, c/h/a/n, or comma-separated): '), (answer) => { | ||
| rl.question('> ', (answer) => { | ||
| rl.close(); | ||
| const selection = this.parseBatchSelection(answer.trim().toLowerCase(), stats); | ||
| resolve(selection); | ||
| const selected = this.parseBatchSelection(answer.trim().toLowerCase(), stats); | ||
| resolve(selected); | ||
| }); | ||
@@ -167,3 +138,3 @@ }); | ||
| /** | ||
| * Parse user's batch selection | ||
| * Parse batch selection | ||
| */ | ||
@@ -173,55 +144,38 @@ parseBatchSelection(input, stats) { | ||
| if (input === 'c') { | ||
| // Critical only: supply-chain, license, security | ||
| return this.batches.filter(b => | ||
| ['supply-chain', 'license', 'security'].includes(b.id) && | ||
| stats[b.id]?.count > 0 | ||
| ); | ||
| return this.batches | ||
| .filter(b => ['supply-chain', 'license', 'security'].includes(b.id)) | ||
| .filter(b => stats[b.id] > 0); | ||
| } | ||
| if (input === 'h') { | ||
| // High priority: critical + quality + ecosystem | ||
| return this.batches.filter(b => | ||
| ['supply-chain', 'license', 'security', 'quality', 'ecosystem'].includes(b.id) && | ||
| stats[b.id]?.count > 0 | ||
| ); | ||
| return this.batches | ||
| .filter(b => ['supply-chain', 'license', 'security', 'quality', 'ecosystem'].includes(b.id)) | ||
| .filter(b => stats[b.id] > 0); | ||
| } | ||
| if (input === 'a') { | ||
| // All safe fixes | ||
| return this.batches.filter(b => stats[b.id]?.count > 0); | ||
| return this.batches.filter(b => stats[b.id] > 0); | ||
| } | ||
| if (input === 'n' || input === '') { | ||
| // None - cancel | ||
| if (input === 'n') { | ||
| return []; | ||
| } | ||
| // Handle comma-separated numbers (e.g., "1,2,5") | ||
| const numbers = input.split(',').map(n => parseInt(n.trim())).filter(n => !isNaN(n)); | ||
| if (numbers.length > 0) { | ||
| return this.batches.filter((b, idx) => | ||
| numbers.includes(idx + 1) && stats[b.id]?.count > 0 | ||
| ); | ||
| } | ||
| // Handle number selections (1-7 or comma-separated) | ||
| const numbers = input.split(',').map(n => parseInt(n.trim())); | ||
| const selectedBatches = []; | ||
| // Invalid input | ||
| return null; | ||
| } | ||
| numbers.forEach(num => { | ||
| if (num >= 1 && num <= this.batches.length) { | ||
| const batch = this.batches[num - 1]; | ||
| if (stats[batch.id] > 0) { | ||
| selectedBatches.push(batch); | ||
| } | ||
| } | ||
| }); | ||
| /** | ||
| * Get batch by ID | ||
| */ | ||
| getBatchById(id) { | ||
| return this.batches.find(b => b.id === id); | ||
| return selectedBatches; | ||
| } | ||
| /** | ||
| * Get all batches with fixes | ||
| */ | ||
| getBatchesWithFixes(stats) { | ||
| return this.batches.filter(b => stats[b.id]?.count > 0); | ||
| } | ||
| } | ||
| module.exports = BatchSelector; |
| { | ||
| "axios": [ | ||
| { | ||
| "title": "Memory leak in request interceptors", | ||
| "severity": "high", | ||
| "affected": ">=1.5.0 <1.6.2", | ||
| "fix": "1.6.2", | ||
| "source": "GitHub Issue #5456", | ||
| "reported": "2024-01-15" | ||
| }, | ||
| { | ||
| "title": "Breaking change in error handling", | ||
| "severity": "medium", | ||
| "affected": ">=1.4.0 <1.5.0", | ||
| "fix": "1.5.0", | ||
| "source": "GitHub Release Notes", | ||
| "reported": "2023-11-20" | ||
| } | ||
| ], | ||
| "lodash": [ | ||
| { | ||
| "title": "Prototype pollution vulnerability", | ||
| "severity": "critical", | ||
| "affected": "<4.17.21", | ||
| "fix": "4.17.21", | ||
| "source": "npm advisory 1523", | ||
| "reported": "2021-02-15" | ||
| } | ||
| ], | ||
| "moment": [ | ||
| { | ||
| "title": "Package is deprecated - no longer maintained", | ||
| "severity": "medium", | ||
| "affected": "*", | ||
| "fix": "Use dayjs or date-fns instead", | ||
| "source": "npm deprecation notice", | ||
| "reported": "2023-09-01" | ||
| } | ||
| ], | ||
| "request": [ | ||
| { | ||
| "title": "Package deprecated - use node-fetch or axios", | ||
| "severity": "high", | ||
| "affected": "*", | ||
| "fix": "Migrate to axios or node-fetch", | ||
| "source": "npm deprecation notice", | ||
| "reported": "2020-02-11" | ||
| } | ||
| ], | ||
| "express": [ | ||
| { | ||
| "title": "Security vulnerability in qs dependency", | ||
| "severity": "medium", | ||
| "affected": ">=4.0.0 <4.18.2", | ||
| "fix": "4.18.2", | ||
| "source": "npm advisory 1867", | ||
| "reported": "2022-11-26" | ||
| } | ||
| ] | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
66
11.86%501444
-3.51%13111
-1.85%48
11.63%