securesync
Version:
Intelligent dependency security scanner with auto-fix
1 lines • 91.8 kB
Source Map (JSON)
{"version":3,"sources":["../src/cli/index.ts","../src/cli/commands/scan.ts","../src/scanner/npm.ts","../src/cli/ui.ts","../src/cli/commands/analyze.ts","../src/analyzer/semver.ts","../src/analyzer/changelog.ts","../src/analyzer/api-diff.ts","../src/analyzer/index.ts","../src/cli/commands/alternatives.ts","../src/alternatives/scorer.ts","../src/alternatives/finder.ts","../src/cli/commands/fix.ts","../src/remediation/migrator.ts","../src/remediation/tester.ts","../src/cli/commands/migrate.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { createScanCommand } from './commands/scan.js';\nimport { createAnalyzeCommand } from './commands/analyze.js';\nimport { createAlternativesCommand } from './commands/alternatives.js';\nimport { createFixCommand } from './commands/fix.js';\nimport { createMigrateCommand } from './commands/migrate.js';\n\nconst program = new Command();\n\nprogram\n .name('securesync')\n .description('Intelligent dependency security scanner with auto-fix')\n .version('1.0.0');\n\n// Add commands\nprogram.addCommand(createScanCommand());\nprogram.addCommand(createFixCommand());\nprogram.addCommand(createAnalyzeCommand());\nprogram.addCommand(createAlternativesCommand());\nprogram.addCommand(createMigrateCommand());\n\n// Parse arguments\nprogram.parse(process.argv);\n","import { Command } from 'commander';\nimport { scanNpmProject } from '../../scanner/index.js';\nimport { ui } from '../ui.js';\n\nexport function createScanCommand(): Command {\n const command = new Command('scan');\n\n command\n .description('Scan project for security vulnerabilities')\n .argument('[path]', 'Project path to scan', process.cwd())\n .option('-d, --dev', 'Include dev dependencies', false)\n .option('-r, --reachability', 'Analyze vulnerability reachability', false)\n .option('--enhance', 'Enhance with additional vulnerability databases', false)\n .option('--fail-on <severity>', 'Exit with error code if vulnerabilities of severity or higher found', '')\n .option('--json', 'Output results as JSON', false)\n .action(async (path: string, options: any) => {\n try {\n ui.startSpinner('Scanning dependencies...');\n\n const results = await scanNpmProject(path, {\n projectPath: path,\n includeDevDependencies: options.dev,\n analyzeReachability: options.reachability,\n enhanceWithOSV: options.enhance,\n });\n\n ui.stopSpinner(true, 'Scan complete');\n\n if (options.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n ui.printScanResults(results);\n }\n\n // Handle fail-on option\n if (options.failOn) {\n const severityLevels = ['low', 'moderate', 'high', 'critical'];\n const failIndex = severityLevels.indexOf(options.failOn.toLowerCase());\n\n if (failIndex === -1) {\n ui.error(`Invalid severity level: ${options.failOn}`);\n process.exit(1);\n }\n\n const hasFailingSeverity = results.vulnerabilities.some(v => {\n const vulnIndex = severityLevels.indexOf(v.severity);\n return vulnIndex >= failIndex;\n });\n\n if (hasFailingSeverity) {\n ui.error(`Found vulnerabilities with severity ${options.failOn} or higher`);\n process.exit(1);\n }\n }\n } catch (error: any) {\n ui.stopSpinner(false, 'Scan failed');\n ui.error(error.message);\n process.exit(1);\n }\n });\n\n return command;\n}\n","import { readFile } from 'fs/promises';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\nimport type {\n ScanResult,\n ScanOptions,\n Vulnerability,\n DependencyTree,\n DependencyNode,\n PackageInfo,\n} from './types.js';\n\nexport async function scanNpmProject(projectPath: string, options: Partial<ScanOptions> = {}): Promise<ScanResult> {\n // 1. Read package.json and package-lock.json\n const packageJson = await readPackageJson(projectPath);\n const lockfile = await readLockfile(projectPath);\n\n // 2. Build dependency tree (including transitive deps)\n const depTree = buildDependencyTree(packageJson, lockfile, options.includeDevDependencies ?? false);\n\n // 3. Query npm audit API for vulnerabilities\n const auditResult = await queryNpmAudit(projectPath, depTree);\n\n // 4. Enhance with OSV and NVD data (if enabled)\n const enhanced = options.enhanceWithOSV\n ? await enhanceVulnerabilities(auditResult)\n : auditResult;\n\n // 5. Calculate reachability (are vulnerable packages used?)\n const vulnerabilities = options.analyzeReachability\n ? await analyzeReachability(enhanced, projectPath)\n : enhanced;\n\n // 6. Build summary\n const summary = {\n critical: vulnerabilities.filter(v => v.severity === 'critical').length,\n high: vulnerabilities.filter(v => v.severity === 'high').length,\n moderate: vulnerabilities.filter(v => v.severity === 'moderate').length,\n low: vulnerabilities.filter(v => v.severity === 'low').length,\n };\n\n return {\n vulnerabilities,\n totalPackages: depTree.packages.length,\n scannedAt: new Date(),\n dependencies: depTree,\n summary,\n };\n}\n\nasync function readPackageJson(projectPath: string): Promise<any> {\n const packageJsonPath = join(projectPath, 'package.json');\n\n if (!existsSync(packageJsonPath)) {\n throw new Error(`package.json not found at ${packageJsonPath}`);\n }\n\n const content = await readFile(packageJsonPath, 'utf-8');\n return JSON.parse(content);\n}\n\nasync function readLockfile(projectPath: string): Promise<any> {\n const lockfilePath = join(projectPath, 'package-lock.json');\n\n if (!existsSync(lockfilePath)) {\n throw new Error(`package-lock.json not found at ${lockfilePath}. Please run 'npm install' first.`);\n }\n\n const content = await readFile(lockfilePath, 'utf-8');\n return JSON.parse(content);\n}\n\nfunction buildDependencyTree(\n packageJson: any,\n lockfile: any,\n includeDevDeps: boolean\n): DependencyTree {\n const packages: PackageInfo[] = [];\n const dependencies = new Map<string, DependencyNode>();\n\n // Process direct dependencies\n const deps = packageJson.dependencies || {};\n const devDeps = includeDevDeps ? (packageJson.devDependencies || {}) : {};\n\n for (const [name] of Object.entries(deps)) {\n const lockEntry = lockfile.packages?.[`node_modules/${name}`];\n if (lockEntry) {\n dependencies.set(name, {\n name,\n version: lockEntry.version,\n resolved: lockEntry.resolved || '',\n dependencies: buildTransitiveDeps(lockfile, lockEntry),\n });\n packages.push({\n name,\n version: lockEntry.version,\n isDevDependency: false,\n isDirect: true,\n });\n }\n }\n\n for (const [name] of Object.entries(devDeps)) {\n const lockEntry = lockfile.packages?.[`node_modules/${name}`];\n if (lockEntry) {\n dependencies.set(name, {\n name,\n version: lockEntry.version,\n resolved: lockEntry.resolved || '',\n dependencies: buildTransitiveDeps(lockfile, lockEntry),\n });\n packages.push({\n name,\n version: lockEntry.version,\n isDevDependency: true,\n isDirect: true,\n });\n }\n }\n\n // Collect all packages (including transitive)\n collectAllPackages(dependencies, packages);\n\n return {\n name: packageJson.name,\n version: packageJson.version,\n dependencies,\n packages,\n };\n}\n\nfunction buildTransitiveDeps(lockfile: any, lockEntry: any): Map<string, DependencyNode> | undefined {\n if (!lockEntry.dependencies) {\n return undefined;\n }\n\n const transitive = new Map<string, DependencyNode>();\n for (const [name] of Object.entries(lockEntry.dependencies)) {\n const transitiveEntry = lockfile.packages?.[`node_modules/${name}`];\n if (transitiveEntry) {\n transitive.set(name, {\n name,\n version: transitiveEntry.version,\n resolved: transitiveEntry.resolved || '',\n dependencies: buildTransitiveDeps(lockfile, transitiveEntry),\n });\n }\n }\n\n return transitive.size > 0 ? transitive : undefined;\n}\n\nfunction collectAllPackages(deps: Map<string, DependencyNode>, packages: PackageInfo[]): void {\n const seen = new Set<string>(packages.map(p => `${p.name}@${p.version}`));\n\n function traverse(depMap: Map<string, DependencyNode> | undefined, isDev: boolean): void {\n if (!depMap) return;\n\n for (const [name, node] of depMap) {\n const key = `${name}@${node.version}`;\n if (!seen.has(key)) {\n seen.add(key);\n packages.push({\n name,\n version: node.version,\n isDevDependency: isDev,\n isDirect: false,\n });\n }\n traverse(node.dependencies, isDev);\n }\n }\n\n for (const [, node] of deps) {\n traverse(node.dependencies, false);\n }\n}\n\nasync function queryNpmAudit(_projectPath: string, _depTree: DependencyTree): Promise<Vulnerability[]> {\n // This would normally call the npm audit API\n // For now, returning a mock implementation structure\n const vulnerabilities: Vulnerability[] = [];\n\n try {\n // In a real implementation, this would:\n // 1. Use npm-registry-fetch to query the audit endpoint\n // 2. Parse the audit report\n // 3. Map vulnerabilities to our format\n\n // Placeholder for actual npm audit API call\n // const auditData = await npmFetch.json('/-/npm/v1/security/audits', {\n // method: 'POST',\n // body: JSON.stringify({ /* lockfile data */ })\n // });\n\n return vulnerabilities;\n } catch (error) {\n console.error('Error querying npm audit:', error);\n return vulnerabilities;\n }\n}\n\nasync function enhanceVulnerabilities(vulnerabilities: Vulnerability[]): Promise<Vulnerability[]> {\n // This would enhance with OSV and NVD data\n // For now, return as-is\n return vulnerabilities;\n}\n\nasync function analyzeReachability(\n vulnerabilities: Vulnerability[],\n _projectPath: string\n): Promise<Vulnerability[]> {\n // This would analyze if vulnerable code is actually imported/used\n // For now, return all vulnerabilities\n return vulnerabilities;\n}\n","import chalk from 'chalk';\nimport ora, { Ora } from 'ora';\nimport type { Vulnerability, ScanResult } from '../scanner/types.js';\nimport type { BreakingChangeAnalysis } from '../analyzer/types.js';\nimport type { Alternative } from '../alternatives/types.js';\n\nexport class UI {\n private spinner: Ora | null = null;\n\n startSpinner(text: string): void {\n this.spinner = ora(text).start();\n }\n\n stopSpinner(success: boolean = true, text?: string): void {\n if (!this.spinner) return;\n\n if (success) {\n this.spinner.succeed(text);\n } else {\n this.spinner.fail(text);\n }\n this.spinner = null;\n }\n\n updateSpinner(text: string): void {\n if (this.spinner) {\n this.spinner.text = text;\n }\n }\n\n success(message: string): void {\n console.log(chalk.green('✓') + ' ' + message);\n }\n\n error(message: string): void {\n console.log(chalk.red('✗') + ' ' + message);\n }\n\n warning(message: string): void {\n console.log(chalk.yellow('⚠') + ' ' + message);\n }\n\n info(message: string): void {\n console.log(chalk.blue('ℹ') + ' ' + message);\n }\n\n header(title: string): void {\n console.log('\\n' + chalk.bold.underline(title) + '\\n');\n }\n\n section(title: string): void {\n console.log('\\n' + chalk.bold(title));\n }\n\n printScanResults(results: ScanResult): void {\n this.header('Security Scan Results');\n\n // Summary\n console.log(chalk.bold('Summary:'));\n console.log(` Total packages scanned: ${results.totalPackages}`);\n console.log(` Vulnerabilities found: ${results.vulnerabilities.length}`);\n console.log(` Scanned at: ${results.scannedAt.toLocaleString()}\\n`);\n\n // Severity breakdown\n if (results.summary) {\n console.log(chalk.bold('Severity Breakdown:'));\n if (results.summary.critical > 0) {\n console.log(` ${chalk.red('Critical')}: ${results.summary.critical}`);\n }\n if (results.summary.high > 0) {\n console.log(` ${chalk.red('High')}: ${results.summary.high}`);\n }\n if (results.summary.moderate > 0) {\n console.log(` ${chalk.yellow('Moderate')}: ${results.summary.moderate}`);\n }\n if (results.summary.low > 0) {\n console.log(` ${chalk.blue('Low')}: ${results.summary.low}`);\n }\n console.log();\n }\n\n // Detailed vulnerabilities\n if (results.vulnerabilities.length > 0) {\n this.section('Vulnerabilities:');\n for (const vuln of results.vulnerabilities) {\n this.printVulnerability(vuln);\n }\n } else {\n this.success('No vulnerabilities found!');\n }\n }\n\n printVulnerability(vuln: Vulnerability): void {\n const severityColor = this.getSeverityColor(vuln.severity);\n const severity = severityColor(vuln.severity.toUpperCase());\n\n console.log(`\\n ${chalk.bold(vuln.id)} [${severity}]`);\n console.log(` Package: ${chalk.cyan(vuln.package)}@${vuln.version}`);\n console.log(` Description: ${vuln.description}`);\n\n if (vuln.patched && vuln.patched.length > 0) {\n console.log(` Patched in: ${chalk.green(vuln.patched.join(', '))}`);\n }\n\n if (vuln.path && vuln.path.length > 0) {\n console.log(` Path: ${vuln.path.join(' > ')}`);\n }\n\n if (vuln.cvss) {\n console.log(` CVSS Score: ${vuln.cvss}`);\n }\n }\n\n printBreakingChanges(analysis: BreakingChangeAnalysis): void {\n this.header(`Breaking Change Analysis: ${analysis.packageName}`);\n\n console.log(` From: ${chalk.cyan(analysis.fromVersion)}`);\n console.log(` To: ${chalk.cyan(analysis.toVersion)}`);\n console.log(` Risk Level: ${this.getRiskColor(analysis.riskLevel)(analysis.riskLevel.toUpperCase())}`);\n console.log(` Breaking Changes: ${analysis.hasBreakingChanges ? chalk.red('YES') : chalk.green('NO')}`);\n console.log(` Changes Found: ${analysis.changes.length}\\n`);\n\n if (analysis.changes.length > 0) {\n this.section('Changes:');\n for (const change of analysis.changes) {\n const typeColor = change.type === 'breaking' ? chalk.red : chalk.green;\n console.log(`\\n ${typeColor(change.type.toUpperCase())}: ${change.symbol}`);\n console.log(` Category: ${change.category}`);\n\n if (change.before) {\n console.log(` Before: ${chalk.gray(change.before)}`);\n }\n if (change.after) {\n console.log(` After: ${chalk.gray(change.after)}`);\n }\n if (change.migration) {\n console.log(` Migration: ${chalk.cyan(change.migration)}`);\n }\n console.log(` Confidence: ${Math.round(change.confidence * 100)}%`);\n }\n }\n }\n\n printAlternatives(alternatives: Alternative[]): void {\n this.header('Alternative Packages');\n\n if (alternatives.length === 0) {\n this.warning('No suitable alternatives found.');\n return;\n }\n\n for (let i = 0; i < alternatives.length; i++) {\n const alt = alternatives[i];\n console.log(`\\n${chalk.bold(`${i + 1}. ${alt.name}`)} (Score: ${alt.score}/100)`);\n console.log(` ${alt.description}`);\n console.log(` Downloads: ${this.formatNumber(alt.downloads)}/week`);\n console.log(` Last publish: ${this.formatDate(alt.lastPublish)}`);\n console.log(` Stars: ${this.formatNumber(alt.stars)}`);\n console.log(` Vulnerabilities: ${alt.vulnerabilities === 0 ? chalk.green('0') : chalk.red(alt.vulnerabilities)}`);\n console.log(` API Compatibility: ${alt.compatibility}%`);\n console.log(` Migration Effort: ${this.getMigrationColor(alt.migrationEffort)(alt.migrationEffort.toUpperCase())}`);\n }\n }\n\n private getSeverityColor(severity: string): typeof chalk.red {\n switch (severity.toLowerCase()) {\n case 'critical':\n case 'high':\n return chalk.red;\n case 'moderate':\n return chalk.yellow;\n case 'low':\n return chalk.blue;\n default:\n return chalk.gray;\n }\n }\n\n private getRiskColor(risk: string): typeof chalk.red {\n switch (risk.toLowerCase()) {\n case 'high':\n return chalk.red;\n case 'medium':\n return chalk.yellow;\n case 'low':\n return chalk.green;\n default:\n return chalk.gray;\n }\n }\n\n private getMigrationColor(effort: string): typeof chalk.red {\n switch (effort.toLowerCase()) {\n case 'low':\n return chalk.green;\n case 'medium':\n return chalk.yellow;\n case 'high':\n return chalk.red;\n default:\n return chalk.gray;\n }\n }\n\n private formatNumber(num: number): string {\n if (num >= 1000000) {\n return (num / 1000000).toFixed(1) + 'M';\n } else if (num >= 1000) {\n return (num / 1000).toFixed(1) + 'K';\n }\n return num.toString();\n }\n\n private formatDate(date: Date): string {\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\n if (diffDays === 0) {\n return 'today';\n } else if (diffDays === 1) {\n return 'yesterday';\n } else if (diffDays < 30) {\n return `${diffDays} days ago`;\n } else if (diffDays < 365) {\n const months = Math.floor(diffDays / 30);\n return `${months} month${months > 1 ? 's' : ''} ago`;\n } else {\n const years = Math.floor(diffDays / 365);\n return `${years} year${years > 1 ? 's' : ''} ago`;\n }\n }\n}\n\nexport const ui = new UI();\n","import { Command } from 'commander';\nimport { analyzeBreakingChanges } from '../../analyzer/index.js';\nimport { ui } from '../ui.js';\n\nexport function createAnalyzeCommand(): Command {\n const command = new Command('analyze');\n\n command\n .description('Analyze breaking changes for a package update')\n .argument('<package>', 'Package name')\n .argument('<from-version>', 'Current version')\n .argument('<to-version>', 'Target version')\n .option('--json', 'Output results as JSON', false)\n .action(async (packageName: string, fromVersion: string, toVersion: string, options: any) => {\n try {\n ui.startSpinner(`Analyzing breaking changes for ${packageName}...`);\n\n const analysis = await analyzeBreakingChanges(\n packageName,\n fromVersion,\n toVersion\n );\n\n ui.stopSpinner(true, 'Analysis complete');\n\n if (options.json) {\n console.log(JSON.stringify(analysis, null, 2));\n } else {\n ui.printBreakingChanges(analysis);\n }\n\n // Exit with error code if breaking changes found\n if (analysis.hasBreakingChanges) {\n process.exit(1);\n }\n } catch (error: any) {\n ui.stopSpinner(false, 'Analysis failed');\n ui.error(error.message);\n process.exit(1);\n }\n });\n\n return command;\n}\n","import { compare, diff, valid, type ReleaseType } from 'semver';\n\nexport interface VersionDiff {\n fromVersion: string;\n toVersion: string;\n diffType: ReleaseType | null;\n isUpgrade: boolean;\n isDowngrade: boolean;\n expectedBreakingChanges: boolean;\n}\n\nexport function analyzeVersionDiff(fromVersion: string, toVersion: string): VersionDiff {\n if (!valid(fromVersion) || !valid(toVersion)) {\n throw new Error(`Invalid semver versions: ${fromVersion} or ${toVersion}`);\n }\n\n const compareResult = compare(fromVersion, toVersion);\n const diffType = diff(fromVersion, toVersion);\n\n return {\n fromVersion,\n toVersion,\n diffType,\n isUpgrade: compareResult < 0,\n isDowngrade: compareResult > 0,\n expectedBreakingChanges: diffType === 'major' || diffType === 'premajor',\n };\n}\n\nexport function shouldAnalyzeBreakingChanges(versionDiff: VersionDiff): boolean {\n // Always analyze if it's a major version bump\n if (versionDiff.expectedBreakingChanges) {\n return true;\n }\n\n // Also analyze minor and patch updates as they might have undeclared breaking changes\n return versionDiff.isUpgrade && versionDiff.diffType !== null;\n}\n","import type { ChangelogEntry } from './types.js';\n\nexport async function parseChangelog(changelogContent: string): Promise<ChangelogEntry[]> {\n const entries: ChangelogEntry[] = [];\n\n // Split by version headers (common formats: ## [1.0.0], # 1.0.0, etc.)\n const versionRegex = /^##?\\s*\\[?(\\d+\\.\\d+\\.\\d+[^\\]]*)\\]?.*$/gm;\n const matches = [...changelogContent.matchAll(versionRegex)];\n\n for (let i = 0; i < matches.length; i++) {\n const match = matches[i];\n const version = match[1];\n const startIndex = match.index! + match[0].length;\n const endIndex = i < matches.length - 1 ? matches[i + 1].index! : changelogContent.length;\n const content = changelogContent.slice(startIndex, endIndex);\n\n entries.push({\n version,\n date: extractDate(match[0]),\n changes: categorizeChanges(content),\n });\n }\n\n return entries;\n}\n\nfunction extractDate(headerLine: string): string | undefined {\n const dateRegex = /\\d{4}-\\d{2}-\\d{2}/;\n const match = headerLine.match(dateRegex);\n return match ? match[0] : undefined;\n}\n\nfunction categorizeChanges(content: string): ChangelogEntry['changes'] {\n const changes = {\n breaking: [] as string[],\n features: [] as string[],\n fixes: [] as string[],\n other: [] as string[],\n };\n\n const lines = content.split('\\n');\n let currentCategory: keyof typeof changes = 'other';\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Detect category headers\n if (/^###?\\s*(breaking|breaking changes)/i.test(trimmed)) {\n currentCategory = 'breaking';\n continue;\n } else if (/^###?\\s*(features?|added)/i.test(trimmed)) {\n currentCategory = 'features';\n continue;\n } else if (/^###?\\s*(fix(es)?|bug\\s*fix(es)?)/i.test(trimmed)) {\n currentCategory = 'fixes';\n continue;\n }\n\n // Extract bullet points\n if (/^[-*]\\s/.test(trimmed)) {\n const change = trimmed.replace(/^[-*]\\s/, '').trim();\n if (change) {\n changes[currentCategory].push(change);\n }\n }\n }\n\n return changes;\n}\n\nexport function extractMigrations(changelog: ChangelogEntry[], targetVersion: string): Map<string, string> {\n const migrations = new Map<string, string>();\n\n const entry = changelog.find(e => e.version === targetVersion);\n if (!entry) {\n return migrations;\n }\n\n // Look for migration hints in breaking changes\n for (const change of entry.changes.breaking) {\n const migrationHint = extractMigrationHint(change);\n if (migrationHint) {\n migrations.set(migrationHint.from, migrationHint.to);\n }\n }\n\n return migrations;\n}\n\nfunction extractMigrationHint(change: string): { from: string; to: string } | null {\n // Look for patterns like \"replace X with Y\" or \"use Y instead of X\"\n const replacePattern = /replace\\s+`?(\\w+)`?\\s+with\\s+`?(\\w+)`?/i;\n const insteadPattern = /use\\s+`?(\\w+)`?\\s+instead\\s+of\\s+`?(\\w+)`?/i;\n\n let match = change.match(replacePattern);\n if (match) {\n return { from: match[1], to: match[2] };\n }\n\n match = change.match(insteadPattern);\n if (match) {\n return { from: match[2], to: match[1] };\n }\n\n return null;\n}\n","import type { APIChange } from './types.js';\n\nexport interface TypeDefinition {\n kind: 'function' | 'class' | 'interface' | 'type' | 'variable';\n name: string;\n signature: string;\n exported: boolean;\n}\n\nexport function diffAPIs(oldTypes: TypeDefinition[], newTypes: TypeDefinition[]): APIChange[] {\n const changes: APIChange[] = [];\n\n // Create maps for efficient lookup\n const oldMap = new Map(oldTypes.map(t => [t.name, t]));\n const newMap = new Map(newTypes.map(t => [t.name, t]));\n\n // Find removed or modified APIs\n for (const [name, oldType] of oldMap) {\n if (!oldType.exported) continue;\n\n const newType = newMap.get(name);\n\n if (!newType) {\n // API was removed\n changes.push({\n type: 'breaking',\n category: 'removed',\n symbol: name,\n before: oldType.signature,\n after: '',\n confidence: 1.0,\n source: 'typescript',\n });\n } else if (newType.signature !== oldType.signature) {\n // API signature changed\n changes.push({\n type: 'breaking',\n category: 'signature',\n symbol: name,\n before: oldType.signature,\n after: newType.signature,\n confidence: 0.9,\n source: 'typescript',\n });\n }\n }\n\n // Find added APIs (features, not breaking)\n for (const [name, newType] of newMap) {\n if (!newType.exported) continue;\n\n if (!oldMap.has(name)) {\n changes.push({\n type: 'feature',\n category: 'signature',\n symbol: name,\n before: '',\n after: newType.signature,\n confidence: 1.0,\n source: 'typescript',\n });\n }\n }\n\n return changes;\n}\n\nexport async function parseTypeDefinitions(packagePath: string): Promise<TypeDefinition[]> {\n // This would parse .d.ts files using TypeScript compiler API\n // For now, return empty array as placeholder\n const definitions: TypeDefinition[] = [];\n\n // In a real implementation:\n // 1. Find .d.ts files in the package\n // 2. Parse them using TypeScript compiler API\n // 3. Extract exported symbols and their signatures\n // 4. Return as TypeDefinition[]\n\n return definitions;\n}\n\nexport function classifyChanges(\n apiDiff: APIChange[],\n migrations: Map<string, string>\n): APIChange[] {\n return apiDiff.map(change => {\n // Add migration hints from changelog\n if (change.category === 'removed' || change.category === 'renamed') {\n const migration = migrations.get(change.symbol);\n if (migration) {\n return {\n ...change,\n migration: `Replace \\`${change.symbol}\\` with \\`${migration}\\``,\n confidence: Math.min(change.confidence + 0.1, 1.0),\n };\n }\n }\n\n return change;\n });\n}\n","import { analyzeVersionDiff, shouldAnalyzeBreakingChanges } from './semver.js';\nimport { parseChangelog, extractMigrations } from './changelog.js';\nimport { diffAPIs, parseTypeDefinitions, classifyChanges } from './api-diff.js';\nimport type { BreakingChangeAnalysis, APIChange } from './types.js';\n\nexport async function analyzeBreakingChanges(\n packageName: string,\n fromVersion: string,\n toVersion: string\n): Promise<BreakingChangeAnalysis> {\n // 1. Compare semantic versions\n const versionDiff = analyzeVersionDiff(fromVersion, toVersion);\n\n if (!shouldAnalyzeBreakingChanges(versionDiff)) {\n return {\n packageName,\n fromVersion,\n toVersion,\n changes: [],\n hasBreakingChanges: false,\n riskLevel: 'low',\n analyzedAt: new Date(),\n };\n }\n\n // 2. Download and analyze both package versions\n const oldPackagePath = await downloadPackage(packageName, fromVersion);\n const newPackagePath = await downloadPackage(packageName, toVersion);\n\n // 3. Parse TypeScript definitions if available\n const oldTypes = await parseTypeDefinitions(oldPackagePath);\n const newTypes = await parseTypeDefinitions(newPackagePath);\n\n // 4. Diff the public API surface\n const apiDiff = diffAPIs(oldTypes, newTypes);\n\n // 5. Parse CHANGELOG.md for migration hints\n const changelog = await loadChangelog(newPackagePath);\n const changelogEntries = changelog ? await parseChangelog(changelog) : [];\n const migrations = extractMigrations(changelogEntries, toVersion);\n\n // 6. Classify changes and add migration hints\n const changes = classifyChanges(apiDiff, migrations);\n\n // Add breaking changes from changelog that weren't detected in API diff\n const changelogBreaking = changelogEntries.find(e => e.version === toVersion);\n if (changelogBreaking) {\n for (const breaking of changelogBreaking.changes.breaking) {\n // Only add if not already detected\n if (!changes.some(c => c.symbol === breaking)) {\n changes.push({\n type: 'breaking',\n category: 'behavior',\n symbol: breaking,\n before: '',\n after: '',\n confidence: 0.7,\n source: 'changelog',\n });\n }\n }\n }\n\n // 7. Calculate risk level\n const riskLevel = calculateRiskLevel(changes, versionDiff.expectedBreakingChanges);\n\n return {\n packageName,\n fromVersion,\n toVersion,\n changes,\n hasBreakingChanges: changes.some(c => c.type === 'breaking'),\n riskLevel,\n analyzedAt: new Date(),\n };\n}\n\nasync function downloadPackage(packageName: string, version: string): Promise<string> {\n // This would use pacote to download the package\n // For now, return a placeholder path\n return `/tmp/securesync-cache/${packageName}@${version}`;\n}\n\nasync function loadChangelog(_packagePath: string): Promise<string | null> {\n // This would read CHANGELOG.md, HISTORY.md, or similar files\n // For now, return null\n return null;\n}\n\nfunction calculateRiskLevel(\n changes: APIChange[],\n expectedBreaking: boolean\n): 'low' | 'medium' | 'high' {\n const breakingChanges = changes.filter(c => c.type === 'breaking');\n\n if (breakingChanges.length === 0) {\n return 'low';\n }\n\n if (breakingChanges.length > 5 || !expectedBreaking) {\n return 'high';\n }\n\n return 'medium';\n}\n\nexport type { APIChange, BreakingChangeAnalysis, ChangelogEntry } from './types.js';\nexport { analyzeVersionDiff } from './semver.js';\nexport { parseChangelog } from './changelog.js';\n","import { Command } from 'commander';\nimport { findAlternatives } from '../../alternatives/index.js';\nimport { ui } from '../ui.js';\n\nexport function createAlternativesCommand(): Command {\n const command = new Command('alternatives');\n\n command\n .description('Find alternative packages')\n .argument('<package>', 'Package name to find alternatives for')\n .option('--min-downloads <number>', 'Minimum weekly downloads', parseInt)\n .option('--max-age <days>', 'Maximum days since last publish', parseInt)\n .option('--min-stars <number>', 'Minimum GitHub stars', parseInt)\n .option('--zero-vulns', 'Only show packages with zero vulnerabilities', false)\n .option('--min-compat <number>', 'Minimum API compatibility score (0-100)', parseInt)\n .option('--json', 'Output results as JSON', false)\n .action(async (packageName: string, options: any) => {\n try {\n ui.startSpinner(`Finding alternatives for ${packageName}...`);\n\n const alternatives = await findAlternatives(packageName, {\n minDownloads: options.minDownloads,\n maxAge: options.maxAge,\n minStars: options.minStars,\n zeroVulnerabilities: options.zeroVulns,\n minCompatibility: options.minCompat,\n });\n\n ui.stopSpinner(true, `Found ${alternatives.length} alternative(s)`);\n\n if (options.json) {\n console.log(JSON.stringify(alternatives, null, 2));\n } else {\n ui.printAlternatives(alternatives);\n }\n } catch (error: any) {\n ui.stopSpinner(false, 'Search failed');\n ui.error(error.message);\n process.exit(1);\n }\n });\n\n return command;\n}\n","import type { PackageMetadata } from './types.js';\n\nexport async function scoreAlternative(\n alternative: PackageMetadata,\n _originalPackage: string\n): Promise<number> {\n let score = 0;\n\n // Popularity (30%)\n const popularityScore = calculatePopularityScore(alternative.downloads);\n score += popularityScore * 0.3;\n\n // Maintenance (25%)\n const maintenanceScore = calculateMaintenanceScore(alternative.lastPublish);\n score += maintenanceScore * 0.25;\n\n // Security (25%)\n const securityScore = await calculateSecurityScore(alternative.name);\n score += securityScore * 0.25;\n\n // Quality indicators (20%)\n const qualityScore = await calculateQualityScore(alternative);\n score += qualityScore * 0.2;\n\n return Math.min(100, Math.round(score));\n}\n\nfunction calculatePopularityScore(downloads: number): number {\n // Scale: 0-100 based on weekly downloads\n // 1M+ downloads = 100\n // 100k downloads = 75\n // 10k downloads = 50\n // 1k downloads = 25\n // <1k downloads = 0-25 scaled\n\n if (downloads >= 1000000) {\n return 100;\n } else if (downloads >= 100000) {\n return 75 + ((downloads - 100000) / 900000) * 25;\n } else if (downloads >= 10000) {\n return 50 + ((downloads - 10000) / 90000) * 25;\n } else if (downloads >= 1000) {\n return 25 + ((downloads - 1000) / 9000) * 25;\n } else {\n return (downloads / 1000) * 25;\n }\n}\n\nfunction calculateMaintenanceScore(lastPublish: Date): number {\n const daysSinceUpdate = daysSince(lastPublish);\n\n // Scale: 0-100 based on recency\n // < 30 days = 100\n // < 90 days = 80\n // < 180 days = 60\n // < 365 days = 40\n // < 730 days = 20\n // > 730 days = 0\n\n if (daysSinceUpdate < 30) {\n return 100;\n } else if (daysSinceUpdate < 90) {\n return 80 + ((90 - daysSinceUpdate) / 60) * 20;\n } else if (daysSinceUpdate < 180) {\n return 60 + ((180 - daysSinceUpdate) / 90) * 20;\n } else if (daysSinceUpdate < 365) {\n return 40 + ((365 - daysSinceUpdate) / 185) * 20;\n } else if (daysSinceUpdate < 730) {\n return 20 + ((730 - daysSinceUpdate) / 365) * 20;\n } else {\n return Math.max(0, 20 - ((daysSinceUpdate - 730) / 365) * 20);\n }\n}\n\nasync function calculateSecurityScore(_packageName: string): Promise<number> {\n // This would query vulnerability databases\n // For now, return a placeholder\n // In real implementation:\n // 1. Query npm audit for this package\n // 2. Query OSV database\n // 3. Check for known security advisories\n // No vulnerabilities = 100\n // 1 low/moderate = 80\n // 1+ high/critical = 0\n\n return 100; // Placeholder\n}\n\nasync function calculateQualityScore(pkg: PackageMetadata): Promise<number> {\n let score = 0;\n\n // Has description\n if (pkg.description && pkg.description.length > 20) {\n score += 20;\n }\n\n // Has repository\n if (pkg.repository) {\n score += 20;\n }\n\n // Has homepage\n if (pkg.homepage) {\n score += 15;\n }\n\n // Has keywords\n if (pkg.keywords && pkg.keywords.length >= 3) {\n score += 15;\n }\n\n // Has license\n if (pkg.license) {\n score += 15;\n }\n\n // TypeScript support (would check for .d.ts files or @types package)\n // score += 15;\n\n return score;\n}\n\nfunction daysSince(date: Date): number {\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n return Math.floor(diff / (1000 * 60 * 60 * 24));\n}\n","import type { Alternative, SearchCriteria, PackageMetadata } from './types.js';\nimport { scoreAlternative } from './scorer.js';\n\nexport async function findAlternatives(\n packageName: string,\n criteria: SearchCriteria = {}\n): Promise<Alternative[]> {\n // 1. Query npm for similar packages\n const keywords = await extractKeywords(packageName);\n const similar = await searchNpm({\n keywords,\n exclude: [packageName],\n });\n\n // 2. Score each alternative\n const scored: Alternative[] = await Promise.all(\n similar.map(async (pkg): Promise<Alternative> => {\n const score = await scoreAlternative(pkg, packageName);\n const compatibility = await analyzeAPICompatibility(pkg, packageName);\n const migrationEffort = determineMigrationEffort(compatibility, score);\n\n return {\n name: pkg.name,\n description: pkg.description,\n downloads: pkg.downloads,\n lastPublish: pkg.lastPublish,\n stars: 0, // TODO: fetch from GitHub API\n issues: 0, // TODO: fetch from GitHub API\n maintainers: 0, // TODO: fetch from npm registry\n vulnerabilities: 0, // TODO: fetch from security databases\n compatibility,\n migrationEffort,\n score,\n };\n })\n );\n\n // 3. Filter and sort by score\n return scored\n .filter(alt => meetsCriteria(alt, criteria))\n .sort((a, b) => b.score - a.score)\n .slice(0, 10);\n}\n\nasync function extractKeywords(packageName: string): Promise<string[]> {\n try {\n const metadata = await fetchPackageMetadata(packageName);\n return metadata.keywords || [];\n } catch {\n // If we can't fetch metadata, derive keywords from package name\n return packageName.split('-').filter(k => k.length > 2);\n }\n}\n\nasync function searchNpm(_options: {\n keywords: string[];\n exclude: string[];\n}): Promise<PackageMetadata[]> {\n // This would use npm-registry-fetch to search for packages\n // For now, return empty array as placeholder\n const results: PackageMetadata[] = [];\n\n // In a real implementation:\n // 1. Build search query from keywords\n // 2. Query npm registry search API\n // 3. Parse and normalize results\n // 4. Fetch additional metadata for each result\n // 5. Filter out excluded packages\n\n return results;\n}\n\nasync function fetchPackageMetadata(packageName: string): Promise<PackageMetadata> {\n // This would use npm-registry-fetch to get package metadata\n // For now, return placeholder data\n return {\n name: packageName,\n description: '',\n version: '1.0.0',\n downloads: 0,\n lastPublish: new Date(),\n keywords: [],\n };\n}\n\nasync function analyzeAPICompatibility(\n _alternative: PackageMetadata,\n _original: string\n): Promise<number> {\n // This would analyze how similar the APIs are\n // Could use:\n // 1. TypeScript definition comparison\n // 2. README/documentation similarity\n // 3. Common function names\n // 4. Similar exports\n\n // For now, return a placeholder value\n return 50; // 0-100 scale\n}\n\nfunction determineMigrationEffort(\n compatibility: number,\n _score: number\n): 'low' | 'medium' | 'high' {\n if (compatibility >= 80) {\n return 'low';\n } else if (compatibility >= 50) {\n return 'medium';\n } else {\n return 'high';\n }\n}\n\nfunction meetsCriteria(alternative: Alternative, criteria: SearchCriteria): boolean {\n if (criteria.minDownloads && alternative.downloads < criteria.minDownloads) {\n return false;\n }\n\n if (criteria.maxAge) {\n const daysSincePublish = daysSince(alternative.lastPublish);\n if (daysSincePublish > criteria.maxAge) {\n return false;\n }\n }\n\n if (criteria.minStars && alternative.stars < criteria.minStars) {\n return false;\n }\n\n if (criteria.zeroVulnerabilities && alternative.vulnerabilities > 0) {\n return false;\n }\n\n if (criteria.minCompatibility && alternative.compatibility < criteria.minCompatibility) {\n return false;\n }\n\n return true;\n}\n\nfunction daysSince(date: Date): number {\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n return Math.floor(diff / (1000 * 60 * 60 * 24));\n}\n","import { Command } from 'commander';\nimport { scanNpmProject } from '../../scanner/index.js';\nimport { analyzeBreakingChanges } from '../../analyzer/index.js';\nimport { generateMigration, testDrivenUpdate } from '../../remediation/index.js';\nimport { ui } from '../ui.js';\nimport inquirer from 'inquirer';\n\nexport function createFixCommand(): Command {\n const command = new Command('fix');\n\n command\n .description('Auto-fix vulnerabilities')\n .argument('[path]', 'Project path', process.cwd())\n .option('--auto', 'Automatically apply fixes without prompts', false)\n .option('--no-test', 'Skip running tests', false)\n .option('--max-severity <level>', 'Only fix vulnerabilities up to this severity', 'critical')\n .option('--breaking-changes <action>', 'How to handle breaking changes (skip, warn, allow)', 'warn')\n .action(async (path: string, options: any) => {\n try {\n // Step 1: Scan for vulnerabilities\n ui.startSpinner('Scanning for vulnerabilities...');\n const scanResults = await scanNpmProject(path, {\n projectPath: path,\n includeDevDependencies: false,\n });\n ui.stopSpinner(true, `Found ${scanResults.vulnerabilities.length} vulnerabilities`);\n\n if (scanResults.vulnerabilities.length === 0) {\n ui.success('No vulnerabilities to fix!');\n return;\n }\n\n // Filter by severity\n const severityLevels = ['low', 'moderate', 'high', 'critical'];\n const maxSeverityIndex = severityLevels.indexOf(options.maxSeverity.toLowerCase());\n const vulnsToFix = scanResults.vulnerabilities.filter(v => {\n const vulnIndex = severityLevels.indexOf(v.severity);\n return vulnIndex >= maxSeverityIndex;\n });\n\n ui.info(`Found ${vulnsToFix.length} vulnerabilities matching severity criteria`);\n\n // Step 2: Group vulnerabilities by package\n const packageUpdates = new Map<string, { current: string; patched: string[] }>();\n for (const vuln of vulnsToFix) {\n if (!packageUpdates.has(vuln.package)) {\n packageUpdates.set(vuln.package, {\n current: vuln.version,\n patched: vuln.patched,\n });\n }\n }\n\n // Step 3: For each package, analyze breaking changes and generate migrations\n for (const [packageName, update] of packageUpdates) {\n const targetVersion = update.patched[0]; // Use first patched version\n if (!targetVersion) {\n ui.warning(`No patched version available for ${packageName}`);\n continue;\n }\n\n ui.section(`Processing ${packageName}@${update.current} -> ${targetVersion}`);\n\n // Analyze breaking changes\n ui.startSpinner('Analyzing breaking changes...');\n const analysis = await analyzeBreakingChanges(\n packageName,\n update.current,\n targetVersion\n );\n ui.stopSpinner(true, 'Analysis complete');\n\n // Handle breaking changes based on policy\n if (analysis.hasBreakingChanges) {\n if (options.breakingChanges === 'skip') {\n ui.warning(`Skipping ${packageName} due to breaking changes`);\n continue;\n } else if (options.breakingChanges === 'warn' && !options.auto) {\n const { proceed } = await inquirer.prompt([\n {\n type: 'confirm',\n name: 'proceed',\n message: `${packageName} has breaking changes. Continue?`,\n default: false,\n },\n ]);\n\n if (!proceed) {\n ui.info(`Skipping ${packageName}`);\n continue;\n }\n }\n }\n\n // Generate migration\n ui.startSpinner('Generating migration scripts...');\n const migrations = await generateMigration(path, packageName, analysis.changes);\n ui.stopSpinner(true, `Generated ${migrations.length} migration(s)`);\n\n // Apply update with tests\n ui.startSpinner('Applying update...');\n const result = await testDrivenUpdate(path, packageName, targetVersion, migrations, {\n autoApply: options.auto,\n runTests: options.test,\n createBackup: true,\n });\n\n if (result.success) {\n ui.stopSpinner(true, `Successfully updated ${packageName}`);\n } else {\n ui.stopSpinner(false, `Failed to update ${packageName}`);\n ui.error(result.reason || 'Unknown error');\n\n if (result.failedTests) {\n ui.error('Failed tests:');\n for (const test of result.failedTests) {\n console.log(` - ${test}`);\n }\n }\n\n if (result.rolledBack) {\n ui.info('Changes have been rolled back');\n }\n }\n }\n\n ui.success('Fix process complete!');\n } catch (error: any) {\n ui.error(error.message);\n process.exit(1);\n }\n });\n\n return command;\n}\n","import { readFile } from 'fs/promises';\nimport { parse } from '@babel/parser';\nimport traverse from '@babel/traverse';\nimport generate from '@babel/generator';\nimport type { APIChange } from '../analyzer/types.js';\nimport type { Migration, CodeChange } from './types.js';\n\nexport async function generateMigration(\n projectPath: string,\n packageName: string,\n changes: APIChange[]\n): Promise<Migration[]> {\n const migrations: Migration[] = [];\n\n // 1. Find all files importing the updated package\n const affectedFiles = await findImports(projectPath, packageName);\n\n for (const file of affectedFiles) {\n // 2. Parse file into AST\n const code = await readFile(file, 'utf-8');\n let ast;\n\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n });\n } catch (error) {\n console.warn(`Failed to parse ${file}:`, error);\n continue;\n }\n\n // 3. Find usages of changed APIs\n const usages: CodeChange[] = [];\n const breakingChanges = changes.filter(c => c.type === 'breaking');\n\n traverse(ast, {\n CallExpression(path) {\n const callee = path.node.callee;\n const affectedChange = findAffectedAPI(callee, breakingChanges);\n\n if (affectedChange) {\n const generated = generate(path.node);\n const replacement = generateReplacement(path.node, affectedChange);\n\n if (replacement) {\n usages.push({\n line: path.node.loc?.start.line || 0,\n column: path.node.loc?.start.column || 0,\n old: generated.code,\n new: replacement,\n });\n }\n }\n },\n ImportDeclaration(path) {\n if (path.node.source.value === packageName) {\n // Check if any imported names have changed\n for (const specifier of path.node.specifiers) {\n if (specifier.type === 'ImportSpecifier') {\n const importedName = specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : '';\n\n const change = breakingChanges.find(\n c => c.symbol === importedName && c.category === 'renamed'\n );\n\n if (change && change.migration) {\n const generated = generate(specifier);\n usages.push({\n line: specifier.loc?.start.line || 0,\n column: specifier.loc?.start.column || 0,\n old: generated.code,\n new: change.migration,\n });\n }\n }\n }\n }\n },\n });\n\n // 4. Generate migration script\n if (usages.length > 0) {\n migrations.push({\n file,\n changes: usages,\n script: generateMigrationScript(file, usages),\n safe: determineSafety(usages, breakingChanges),\n });\n }\n }\n\n return migrations;\n}\n\nasync function findImports(_projectPath: string, _packageName: string): Promise<string[]> {\n // This would use a tool like grep or a custom file walker\n // to find all files importing the package\n // For now, return empty array\n return [];\n}\n\nfunction findAffectedAPI(callee: any, changes: APIChange[]): APIChange | null {\n if (callee.type === 'Identifier') {\n return changes.find(c => c.symbol === callee.name) || null;\n }\n\n if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {\n return changes.find(c => c.symbol === callee.property.name) || null;\n }\n\n return null;\n}\n\nfunction generateReplacement(_node: any, change: APIChange): string | null {\n if (change.migration) {\n // If there's an explicit migration hint, use it\n return change.migration;\n }\n\n // Try to infer a replacement based on the change type\n if (change.category === 'signature') {\n // Signature changed but we don't have a migration hint\n // Return null to indicate manual intervention needed\n return null;\n }\n\n return null;\n}\n\nfunction generateMigrationScript(file: string, changes: CodeChange[]): string {\n let script = `// Migration script for ${file}\\n\\n`;\n\n script += `import { readFile, writeFile } from 'fs/promises';\\n\\n`;\n script += `async function migrate() {\\n`;\n script += ` const content = await readFile('${file}', 'utf-8');\\n`;\n script += ` let updated = content;\\n\\n`;\n\n for (const change of changes) {\n script += ` // Line ${change.line}: ${change.old} -> ${change.new}\\n`;\n script += ` updated = updated.replace(\\n`;\n script += ` ${JSON.stringify(change.old)},\\n`;\n script += ` ${JSON.stringify(change.new)}\\n`;\n script += ` );\\n\\n`;\n }\n\n script += ` await writeFile('${file}', updated, 'utf-8');\\n`;\n script += ` console.log('Migration completed for ${file}');\\n`;\n script += `}\\n\\n`;\n script += `migrate().catch(console.error);\\n`;\n\n return script;\n}\n\nfunction determineSafety(changes: CodeChange[], apiChanges: APIChange[]): boolean {\n // Consider it safe if:\n // 1. All changes have high confidence (>0.8)\n // 2. All changes have migration hints\n // 3. Number of changes is small (<= 5)\n\