Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement โ†’
Sign In

devcompass

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

devcompass - npm Package Compare versions

Comparing version
3.1.6
to
3.1.7
+51
data/batch-categories.json
[
{
"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);

// 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 @@

// 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 @@

// 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
};
// 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"
}
]
}