diff --git a/.gitignore b/.gitignore index e43e894..6cdb1e4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* +reporter.ts.backup +vulnify-report.json # Build outputs dist/ build/ diff --git a/package-lock.json b/package-lock.json index 699a38c..d85ac8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vulnify", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vulnify", - "version": "1.0.0", + "version": "1.0.2", "license": "MIT", "dependencies": { "axios": "^1.11.0", diff --git a/package.json b/package.json index 1067fba..5995f3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vulnify", - "version": "1.0.0", + "version": "1.0.2", "description": "CLI tool for vulnerability analysis using Vulnify SCA API - similar to Snyk CLI", "main": "dist/index.js", "bin": { diff --git a/src/cli.ts b/src/cli.ts index 12af75e..adab96a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,6 +4,7 @@ import { Command } from 'commander'; import { testCommand } from './commands/test'; import { helpCommand } from './commands/help'; import { infoCommand } from './commands/info'; +import { pingCommand } from './commands/ping'; import { colors } from './utils/colors'; const program = new Command(); @@ -12,10 +13,11 @@ const program = new Command(); program .name('vulnify') .description('CLI tool for vulnerability analysis using Vulnify SCA API') - .version('1.0.0', '-v, --version', 'display version number'); + .version('1.0.2', '-v, --version', 'display version number'); // Add commands program.addCommand(testCommand); +program.addCommand(pingCommand); program.addCommand(helpCommand); program.addCommand(infoCommand); @@ -26,6 +28,7 @@ program.on('--help', () => { console.log(' $ vulnify test # Analyze current project'); console.log(' $ vulnify test --file package.json # Analyze specific file'); console.log(' $ vulnify test --ecosystem npm # Force ecosystem detection'); + console.log(' $ vulnify ping # Test API connectivity'); console.log(' $ vulnify info # Show API information'); console.log(''); console.log(colors.muted('For more information, visit: https://docs.vulnify.io')); diff --git a/src/commands/index.ts b/src/commands/index.ts index dc75191..0c81b60 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,4 +1,5 @@ export * from './test'; export * from './help'; export * from './info'; +export * from './ping'; diff --git a/src/commands/ping.ts b/src/commands/ping.ts new file mode 100644 index 0000000..61631ed --- /dev/null +++ b/src/commands/ping.ts @@ -0,0 +1,98 @@ +import { Command } from 'commander'; +import { colors } from '../utils/colors'; +import { createSpinner } from '../utils/spinner'; +import { logger } from '../utils/logger'; +import { config } from '../utils/config'; +import { ApiClient } from '../services/api'; + +/** + * Ping command to test API connectivity + */ +export const pingCommand = new Command('ping') + .description('Test connectivity to vulnerability analysis API') + .option('--verbose', 'show detailed connection information') + .action(async (options) => { + console.log(colors.title('πŸ“ Vulnify API Connectivity Test')); + console.log(''); + + if (options.verbose) { + console.log(colors.muted('Configuration:')); + console.log(colors.muted(` API URL: ${config.getApiUrl()}`)); + console.log(colors.muted(` Timeout: ${config.getTimeout()}ms`)); + console.log(''); + } + + const spinner = createSpinner('πŸ” Testing API connectivity...'); + spinner.start(); + + try { + // Test connectivity to API - simple HTTP check + const apiClient = new ApiClient(); + const startTime = Date.now(); + + // Try to make a simple request to test connectivity + // We'll catch the error but if we get a response (even an error), it means the API is reachable + try { + await apiClient.analyze({ + ecosystem: 'npm', + dependencies: [] + }); + } catch (error) { + // If we get a validation error, it means the API is reachable + if (error instanceof Error && ( + error.message.includes('dependencies') || + error.message.includes('At least one dependency is required') + )) { + // This is expected - empty dependencies array causes validation error + // but it means the API is responding + } else { + throw error; + } + } + + const responseTime = Date.now() - startTime; + + spinner.stop(); + console.log(''); + + // Display API results + console.log(colors.info('πŸ“‘ API Service:')); + console.log(colors.success(` βœ… Available (${responseTime}ms)`)); + + // Overall status + console.log(''); + console.log(colors.success('πŸŽ‰ API service is available!')); + console.log(colors.info('πŸ’‘ Ready to analyze dependencies for vulnerabilities')); + + console.log(''); + console.log(colors.muted('Use "vulnify test" to start analyzing your project')); + + } catch (error) { + spinner.fail('❌ Connectivity test failed'); + + console.log(''); + console.log(colors.error('Error details:')); + if (error instanceof Error) { + console.log(colors.error(` ${error.message}`)); + } else { + console.log(colors.error(' Unknown error occurred')); + } + + console.log(''); + console.log(colors.warning('πŸ’‘ Troubleshooting:')); + console.log(' β€’ Check your internet connection'); + console.log(' β€’ Verify the API endpoint is accessible'); + console.log(' β€’ Try again in a few moments'); + console.log(' β€’ Use --verbose for more details'); + + if (options.verbose) { + logger.error('Ping command failed', { + error: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined + }); + } + + process.exit(1); + } + }); + diff --git a/src/commands/test.ts b/src/commands/test.ts index cb40771..e3ac001 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1,28 +1,162 @@ import { Command } from 'commander'; -import { CliOptions, Ecosystem } from '../types/cli'; +import { EnhancedCliOptions, Ecosystem, ValidationResult } from '../types/cli'; import { colors } from '../utils/colors'; import { createSpinner } from '../utils/spinner'; -import { logger } from '../utils/logger'; +import { logger, LogLevel } from '../utils/logger'; import { config } from '../utils/config'; import { DependencyDetector } from '../services/detector'; import { DependencyParser } from '../services/parser'; -import { ApiClient } from '../services/api'; +import { mongoApiClient } from '../services/mongoApi'; import { ReportGenerator } from '../services/reporter'; +import * as fs from 'fs-extra'; +import * as path from 'path'; + +/** + * Validate file parameter + */ +const validateFileParameter = async (filePath: string): Promise => { + if (!filePath) { + return { valid: false, message: 'File path is required' }; + } + + const absolutePath = path.resolve(filePath); + + if (!await fs.pathExists(absolutePath)) { + return { + valid: false, + message: `File not found: ${filePath}`, + suggestions: [ + 'Check if the file path is correct', + 'Ensure the file exists in the specified location', + 'Use relative or absolute paths' + ] + }; + } + + const stats = await fs.stat(absolutePath); + if (!stats.isFile()) { + return { + valid: false, + message: `Path is not a file: ${filePath}`, + suggestions: ['Specify a file, not a directory'] + }; + } + + return { valid: true }; +}; + +/** + * Validate ecosystem parameter + */ +const validateEcosystemParameter = (ecosystem: string): ValidationResult => { + const supportedEcosystems = ['npm', 'pypi', 'maven', 'nuget', 'rubygems', 'composer', 'go', 'cargo']; + + if (!ecosystem) { + return { valid: false, message: 'Ecosystem is required' }; + } + + if (!supportedEcosystems.includes(ecosystem)) { + return { + valid: false, + message: `Unsupported ecosystem: ${ecosystem}`, + suggestions: [ + `Supported ecosystems: ${supportedEcosystems.join(', ')}`, + 'Use --help to see all available options' + ] + }; + } + + return { valid: true }; +}; + +/** + * Validate output parameter + */ +const validateOutputParameter = (output: string): ValidationResult => { + const supportedFormats = ['json', 'table', 'summary', 'enhanced']; + + if (!output) { + return { valid: true }; // Optional parameter + } + + if (!supportedFormats.includes(output)) { + return { + valid: false, + message: `Unsupported output format: ${output}`, + suggestions: [ + `Supported formats: ${supportedFormats.join(', ')}`, + 'Use --help to see format descriptions' + ] + }; + } + + return { valid: true }; +}; + +/** + * Display validation errors with suggestions + */ +const displayValidationError = (result: ValidationResult): void => { + console.log(''); + console.log(colors.error('❌ Validation Error')); + console.log(colors.error(result.message || 'Unknown validation error')); + + if (result.suggestions && result.suggestions.length > 0) { + console.log(''); + console.log(colors.warning('πŸ’‘ Suggestions:')); + result.suggestions.forEach(suggestion => { + console.log(` β€’ ${suggestion}`); + }); + } + console.log(''); +}; + export const testCommand = new Command('test') .description('Analyze project dependencies for vulnerabilities') .option('-f, --file ', 'specify dependency file to analyze') - .option('-e, --ecosystem ', 'force ecosystem detection') - .option('-o, --output ', 'output format: json, table, summary', 'table') + .option('-e, --ecosystem ', 'force ecosystem detection (npm, pypi, maven, nuget, rubygems, composer, go, cargo)') + .option('-o, --output ', 'output format: enhanced, table, json, summary', 'enhanced') .option('-s, --severity ', 'filter by severity: critical, high, medium, low') .option('-k, --api-key ', 'API key for increased rate limits') .option('-t, --timeout ', 'request timeout in milliseconds', '30000') + .option('--max-depth ', 'maximum directory depth for recursive search', '3') + .option('--verbose', 'enable verbose logging') .option('--no-report', 'skip generating report.json file') - .action(async (options: CliOptions) => { - console.log(colors.title('Vulnify - Vulnerability Analysis')); + .action(async (options: EnhancedCliOptions) => { + console.log(colors.title('πŸ” Vulnify - Advanced Vulnerability Analysis')); console.log(''); - const spinner = createSpinner('Initializing analysis...'); + // Enable verbose logging if requested + if (options.verbose) { + logger.setLevel(LogLevel.DEBUG); + logger.debug('Verbose logging enabled'); + } + + // Validate input parameters + if (options.file) { + const fileValidation = await validateFileParameter(options.file); + if (!fileValidation.valid) { + displayValidationError(fileValidation); + process.exit(1); + } + } + + if (options.ecosystem) { + const ecosystemValidation = validateEcosystemParameter(options.ecosystem); + if (!ecosystemValidation.valid) { + displayValidationError(ecosystemValidation); + process.exit(1); + } + } + + const outputValidation = validateOutputParameter(options.output || 'enhanced'); + if (!outputValidation.valid) { + displayValidationError(outputValidation); + process.exit(1); + } + + const spinner = createSpinner('πŸš€ Initializing analysis...'); spinner.start(); try { @@ -34,10 +168,10 @@ export const testCommand = new Command('test') config.set('timeout', parseInt(options.timeout.toString(), 10)); } - // Initialize services - const detector = new DependencyDetector(); + // Initialize services with enhanced options + const maxDepth = options.maxDepth ? parseInt(options.maxDepth.toString(), 10) : 3; + const detector = new DependencyDetector(process.cwd(), maxDepth); const parser = new DependencyParser(); - const apiClient = new ApiClient(); const reporter = new ReportGenerator(); let filePath: string; @@ -45,7 +179,7 @@ export const testCommand = new Command('test') if (options.file) { // Analyze specific file - spinner.updateText(`Analyzing file: ${options.file}`); + spinner.updateText(`πŸ“„ Analyzing file: ${options.file}`); const detectedFile = await detector.detectFile(options.file); if (!detectedFile) { @@ -54,13 +188,30 @@ export const testCommand = new Command('test') filePath = detectedFile.path; ecosystem = options.ecosystem as Ecosystem || detectedFile.ecosystem; + + logger.info(`File analysis: ${path.basename(filePath)} (${detector.getEcosystemDisplayName(ecosystem)})`); } else { - // Auto-detect project files - spinner.updateText('Detecting project dependencies...'); + // Auto-detect project files with enhanced search + spinner.updateText('πŸ” Detecting project dependencies...'); const detectedFiles = await detector.detectFiles(); if (detectedFiles.length === 0) { - throw new Error('No dependency files found in current directory. Supported files: package.json, requirements.txt, pom.xml, etc.'); + throw new Error(`No dependency files found in current directory or subdirectories. + +πŸ’‘ Supported files: + β€’ Node.js: package.json, package-lock.json, yarn.lock + β€’ Python: requirements.txt, Pipfile, pyproject.toml, setup.py + β€’ Java: pom.xml, build.gradle, build.gradle.kts + β€’ .NET: packages.config, *.csproj, *.fsproj, *.vbproj + β€’ Ruby: Gemfile, Gemfile.lock + β€’ PHP: composer.json, composer.lock + β€’ Go: go.mod, go.sum + β€’ Rust: Cargo.toml, Cargo.lock + +πŸ”§ Try: + β€’ Use --file option to specify a dependency file + β€’ Use --max-depth to increase search depth + β€’ Check if you're in the correct project directory`); } const bestFile = detector.getBestFile(detectedFiles, options.ecosystem as Ecosystem); @@ -71,23 +222,37 @@ export const testCommand = new Command('test') filePath = bestFile.path; ecosystem = bestFile.ecosystem as Ecosystem; - logger.info(`Detected ${ecosystem} project: ${bestFile.path}`); + const relativePath = path.relative(process.cwd(), bestFile.path); + logger.info(`Detected ${detector.getEcosystemDisplayName(ecosystem)} project: ${relativePath}`); + + // Show additional detected files if verbose + if (options.verbose && detectedFiles.length > 1) { + console.log(''); + console.log(colors.muted('πŸ“‹ All detected files:')); + detectedFiles.forEach(file => { + const rel = path.relative(process.cwd(), file.path); + const confidence = Math.round(file.confidence * 100); + console.log(colors.muted(` β€’ ${rel} (${file.ecosystem}, ${confidence}% confidence)`)); + }); + } } + // Parse dependencies - spinner.updateText('Parsing dependencies...'); + spinner.updateText('πŸ“¦ Parsing dependencies...'); const parsedDeps = await parser.parseFile(filePath, ecosystem); if (parsedDeps.dependencies.length === 0) { - throw new Error('No dependencies found in the specified file'); + throw new Error(`No dependencies found in the specified file: ${path.basename(filePath)}`); } - logger.info(`Found ${parsedDeps.dependencies.length} dependencies`); + const depCount = parsedDeps.dependencies.length; + logger.info(`Found ${depCount} dependencies in ${detector.getEcosystemDisplayName(ecosystem)} project`); // Analyze vulnerabilities - spinner.updateText('Analyzing vulnerabilities...'); + spinner.updateText(`πŸ” Analyzing ${depCount} dependencies for vulnerabilities...`); - const analysisResponse = await apiClient.analyze({ + const analysisResponse = await mongoApiClient.analyze({ ecosystem: parsedDeps.ecosystem, dependencies: parsedDeps.dependencies }); @@ -98,8 +263,6 @@ export const testCommand = new Command('test') const results = analysisResponse.results; - spinner.succeed(`Analysis completed - Found ${results.vulnerabilities_found} vulnerabilities`); - // Filter by severity if specified if (options.severity) { const severityLevels = ['low', 'medium', 'high', 'critical']; @@ -127,7 +290,18 @@ export const testCommand = new Command('test') } } - // Display results + const vulnCount = results.vulnerabilities_found; + const scanTime = results.scan_time; + + if (vulnCount > 0) { + spinner.fail(`⚠️ Analysis completed - Found ${vulnCount} vulnerabilities in ${scanTime}`); + } else { + spinner.succeed(`βœ… Analysis completed - No vulnerabilities found in ${scanTime}`); + } + + console.log(''); + + // Display results with enhanced formatting switch (options.output) { case 'json': reporter.displayJsonResults(results); @@ -135,6 +309,10 @@ export const testCommand = new Command('test') case 'summary': reporter.displaySummaryResults(results); break; + case 'enhanced': + // For now, fall back to table format until we implement enhanced display + reporter.displayTableResults(results); + break; case 'table': default: reporter.displayTableResults(results); @@ -150,7 +328,7 @@ export const testCommand = new Command('test') }, config.getReportFilename()); console.log(''); - console.log(colors.success(`πŸ“„ Report saved: ${reportPath}`)); + console.log(colors.success(`πŸ“„ Report saved: ${path.relative(process.cwd(), reportPath)}`)); } catch (error) { logger.warn('Failed to generate JSON report:', error instanceof Error ? error.message : 'Unknown error'); } @@ -166,29 +344,34 @@ export const testCommand = new Command('test') } } catch (error) { - spinner.fail('Analysis failed'); + spinner.fail('❌ Analysis failed'); if (error instanceof Error) { - logger.error(error.message); + console.log(''); + console.log(colors.error('Error: ' + error.message)); // Provide helpful suggestions based on error type if (error.message.includes('No dependency files found')) { - console.log(''); - console.log(colors.warning('πŸ’‘ Suggestions:')); - console.log(' β€’ Make sure you are in a project directory'); - console.log(' β€’ Use --file option to specify a dependency file'); - console.log(' β€’ Supported files: package.json, requirements.txt, pom.xml, composer.json, etc.'); + // Error message already includes suggestions } else if (error.message.includes('Network Error') || error.message.includes('ENOTFOUND')) { console.log(''); - console.log(colors.warning('πŸ’‘ Suggestions:')); + console.log(colors.warning('πŸ’‘ Network Issues:')); console.log(' β€’ Check your internet connection'); console.log(' β€’ Verify the API endpoint is accessible'); console.log(' β€’ Try again in a few moments'); + console.log(' β€’ Use --timeout to increase request timeout'); } else if (error.message.includes('Rate Limit')) { console.log(''); - console.log(colors.warning('πŸ’‘ Suggestions:')); + console.log(colors.warning('πŸ’‘ Rate Limit Exceeded:')); console.log(' β€’ Use --api-key option to get higher rate limits'); console.log(' β€’ Wait a moment before trying again'); + console.log(' β€’ Consider upgrading your API plan'); + } else if (error.message.includes('timeout')) { + console.log(''); + console.log(colors.warning('πŸ’‘ Timeout Issues:')); + console.log(' β€’ Use --timeout option to increase timeout'); + console.log(' β€’ Check your network connection'); + console.log(' β€’ Try analyzing fewer dependencies at once'); } } diff --git a/src/services/api.ts b/src/services/api.ts index 60060dd..7a9118b 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -64,7 +64,7 @@ export class ApiClient { */ async analyze(request: AnalysisRequest): Promise { try { - const response: AxiosResponse = await this.client.post('/analyze', request); + const response: AxiosResponse = await this.client.post('api/v1/analyze', request); return response.data; } catch (error) { throw this.handleError(error); @@ -76,48 +76,61 @@ export class ApiClient { */ async autoAnalyze(request: AutoAnalysisRequest): Promise { try { - const response: AxiosResponse = await this.client.post('/analyze/auto', request); + const response: AxiosResponse = await this.client.post('api/v1/analyze/auto', request); return response.data; } catch (error) { throw this.handleError(error); } } - - /** - * Get API statistics - */ - async getStats(): Promise { - try { - const response: AxiosResponse = await this.client.get('/analyze/stats'); - return response.data; - } catch (error) { - throw this.handleError(error); - } +/** + * Get API information + */ +async getInfo(): Promise { + try { + const response = await this.client.request({ + url: '/api/v1/info', + method: 'GET', + data: {} // forΓ§a envio de JSON vazio + }); + return response.data; + } catch (error) { + throw this.handleError(error); } +} - /** - * Get API information - */ - async getInfo(): Promise { - try { - const response: AxiosResponse = await this.client.get('/info'); - return response.data; - } catch (error) { - throw this.handleError(error); - } +/** + * Get API statistics and health information + */ +async getStats(): Promise { + try { + const response = await this.client.request({ + url: '/api/v1/stats', + method: 'GET', + data: {} // forΓ§a envio de JSON vazio + }); + return response.data; + } catch (error) { + throw this.handleError(error); } +} - /** - * Health check - */ - async healthCheck(): Promise<{ status: string; timestamp: string }> { - try { - const response = await this.client.get('/health'); - return response.data; - } catch (error) { - throw this.handleError(error); - } +/** + * Health check + */ +async healthCheck(): Promise<{ status: string; timestamp: string }> { + try { + const response = await this.client.request({ + url: '/api/v1/health', + method: 'GET', + data: {} // forΓ§a envio de JSON vazio + }); + return response.data; + } catch (error) { + throw this.handleError(error); } +} + + /** * Handle API errors and convert to user-friendly messages diff --git a/src/services/detector.ts b/src/services/detector.ts index 491fab9..7a646f3 100644 --- a/src/services/detector.ts +++ b/src/services/detector.ts @@ -1,6 +1,6 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { DetectedFile, Ecosystem, EcosystemConfig } from '../types/cli'; +import { DetectedFile, Ecosystem, EcosystemConfig, ProjectStructure } from '../types/cli'; import { logger } from '../utils/logger'; // Configuration for each ecosystem @@ -87,79 +87,166 @@ const ECOSYSTEM_CONFIGS: EcosystemConfig[] = [ } ]; +// Directories to ignore during recursive search +const IGNORE_DIRECTORIES = [ + 'node_modules', + '.git', + '.svn', + '.hg', + 'dist', + 'build', + 'target', + 'bin', + 'obj', + '__pycache__', + '.pytest_cache', + 'venv', + 'env', + '.env', + '.vscode', + '.idea', + 'coverage', + '.nyc_output', + 'logs', + 'tmp', + 'temp' +]; + export class DependencyDetector { private projectPath: string; + private maxDepth: number; - constructor(projectPath: string = process.cwd()) { + constructor(projectPath: string = process.cwd(), maxDepth: number = 3) { this.projectPath = path.resolve(projectPath); + this.maxDepth = maxDepth; } /** - * Detect dependency files in the project directory + * Detect dependency files in the project directory with recursive search */ async detectFiles(): Promise { const detectedFiles: DetectedFile[] = []; - logger.debug(`Scanning for dependency files in: ${this.projectPath}`); + logger.debug(`Scanning for dependency files in: ${this.projectPath} (max depth: ${this.maxDepth})`); + + // First, try current directory (highest priority) + await this.scanDirectory(this.projectPath, detectedFiles, 0); + + // If no files found in current directory, search recursively + if (detectedFiles.length === 0) { + logger.info('No dependency files found in current directory, searching subdirectories...'); + await this.scanDirectoryRecursive(this.projectPath, detectedFiles, 1); + } + + // Special handling for .NET projects + await this.detectDotNetProjects(detectedFiles); + + // Detect project structure for better analysis + const projectStructure = await this.detectProjectStructure(detectedFiles); + if (projectStructure.isMonorepo) { + logger.info(`Detected monorepo structure with ${projectStructure.subprojects.length} subprojects`); + } + + return this.prioritizeFiles(detectedFiles); + } + + /** + * Recursive directory scanning with depth limit + */ + private async scanDirectoryRecursive( + dirPath: string, + detectedFiles: DetectedFile[], + currentDepth: number + ): Promise { + if (currentDepth > this.maxDepth) { + return; + } + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + const subDirPath = path.join(dirPath, entry.name); + + // Skip ignored directories + if (IGNORE_DIRECTORIES.includes(entry.name)) { + continue; + } + + // Scan subdirectory + await this.scanDirectory(subDirPath, detectedFiles, currentDepth); + + // Continue recursive search + await this.scanDirectoryRecursive(subDirPath, detectedFiles, currentDepth + 1); + } + } + } catch (error) { + logger.debug(`Error scanning directory ${dirPath}:`, error); + } + } + + /** + * Scan a single directory for dependency files + */ + private async scanDirectory( + dirPath: string, + detectedFiles: DetectedFile[], + depth: number + ): Promise { for (const config of ECOSYSTEM_CONFIGS) { // Check primary files for (const fileName of config.files.primary) { - const filePath = path.join(this.projectPath, fileName); + const filePath = path.join(dirPath, fileName); if (await fs.pathExists(filePath)) { detectedFiles.push({ path: filePath, ecosystem: config.name, - confidence: 0.9, + confidence: depth === 0 ? 0.9 : Math.max(0.5, 0.9 - (depth * 0.2)), type: 'primary' }); - logger.debug(`Found primary file: ${fileName} (${config.name})`); + logger.debug(`Found primary file: ${fileName} (${config.name}) at depth ${depth}`); } } // Check lockfiles for (const fileName of config.files.lockfiles) { - const filePath = path.join(this.projectPath, fileName); + const filePath = path.join(dirPath, fileName); if (await fs.pathExists(filePath)) { detectedFiles.push({ path: filePath, ecosystem: config.name, - confidence: 0.7, + confidence: depth === 0 ? 0.7 : Math.max(0.3, 0.7 - (depth * 0.2)), type: 'lockfile' }); - logger.debug(`Found lockfile: ${fileName} (${config.name})`); + logger.debug(`Found lockfile: ${fileName} (${config.name}) at depth ${depth}`); } } // Check config files for (const fileName of config.files.config) { - const filePath = path.join(this.projectPath, fileName); + const filePath = path.join(dirPath, fileName); if (await fs.pathExists(filePath)) { detectedFiles.push({ path: filePath, ecosystem: config.name, - confidence: 0.3, + confidence: depth === 0 ? 0.3 : Math.max(0.1, 0.3 - (depth * 0.1)), type: 'config' }); - logger.debug(`Found config file: ${fileName} (${config.name})`); + logger.debug(`Found config file: ${fileName} (${config.name}) at depth ${depth}`); } } } - - // Special handling for .NET projects (scan for *.csproj, *.fsproj, *.vbproj) - await this.detectDotNetProjects(detectedFiles); - - return this.prioritizeFiles(detectedFiles); } /** - * Detect specific file by path + * Detect specific file by path with enhanced validation */ async detectFile(filePath: string): Promise { const absolutePath = path.resolve(filePath); if (!await fs.pathExists(absolutePath)) { - return null; + throw new Error(`File not found: ${filePath}`); } const fileName = path.basename(absolutePath); @@ -213,7 +300,7 @@ export class DependencyDetector { } /** - * Get the best file for analysis from detected files + * Get the best file for analysis from detected files with enhanced logic */ getBestFile(detectedFiles: DetectedFile[], ecosystem?: Ecosystem): DetectedFile | null { if (detectedFiles.length === 0) { @@ -229,62 +316,180 @@ export class DependencyDetector { candidates = detectedFiles; } - // Prioritize by type and confidence + // Prioritize by type, confidence, and path depth candidates.sort((a, b) => { // Primary files first if (a.type === 'primary' && b.type !== 'primary') return -1; if (b.type === 'primary' && a.type !== 'primary') return 1; // Then by confidence - return b.confidence - a.confidence; + if (Math.abs(a.confidence - b.confidence) > 0.1) { + return b.confidence - a.confidence; + } + + // Prefer files closer to project root + const aDepth = a.path.split(path.sep).length; + const bDepth = b.path.split(path.sep).length; + if (aDepth !== bDepth) { + return aDepth - bDepth; + } + + // Then by ecosystem preference + const ecosystemPriority = ['npm', 'pypi', 'maven', 'nuget', 'rubygems', 'composer', 'go', 'cargo']; + const aIndex = ecosystemPriority.indexOf(a.ecosystem); + const bIndex = ecosystemPriority.indexOf(b.ecosystem); + + return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex); }); return candidates[0]; } /** - * Detect .NET project files + * Detect project structure and identify monorepos + */ + private async detectProjectStructure(detectedFiles: DetectedFile[]): Promise { + const structure: ProjectStructure = { + isMonorepo: false, + rootEcosystem: null, + subprojects: [], + totalFiles: detectedFiles.length + }; + + // Group files by directory + const filesByDir = new Map(); + + for (const file of detectedFiles) { + const dir = path.dirname(file.path); + if (!filesByDir.has(dir)) { + filesByDir.set(dir, []); + } + filesByDir.get(dir)!.push(file); + } + + // Detect monorepo if multiple directories have primary files + const dirsWithPrimary = Array.from(filesByDir.entries()) + .filter(([_, files]) => files.some(f => f.type === 'primary')); + + if (dirsWithPrimary.length > 1) { + structure.isMonorepo = true; + structure.subprojects = dirsWithPrimary.map(([dir, files]) => ({ + path: dir, + ecosystem: files.find(f => f.type === 'primary')?.ecosystem || 'unknown', + files: files.length + })); + } + + // Determine root ecosystem + const rootFiles = filesByDir.get(this.projectPath) || []; + const rootPrimary = rootFiles.find(f => f.type === 'primary'); + if (rootPrimary) { + structure.rootEcosystem = rootPrimary.ecosystem; + } + + return structure; + } + + /** + * Detect .NET project files with improved scanning */ private async detectDotNetProjects(detectedFiles: DetectedFile[]): Promise { try { - const files = await fs.readdir(this.projectPath); + await this.scanForDotNetFiles(this.projectPath, detectedFiles, 0); + } catch (error) { + logger.debug('Error scanning for .NET projects:', error); + } + } + + /** + * Recursively scan for .NET project files + */ + private async scanForDotNetFiles( + dirPath: string, + detectedFiles: DetectedFile[], + depth: number + ): Promise { + if (depth > this.maxDepth) return; + + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); - for (const file of files) { - const ext = path.extname(file); - if (['.csproj', '.fsproj', '.vbproj'].includes(ext)) { - const filePath = path.join(this.projectPath, file); - detectedFiles.push({ - path: filePath, - ecosystem: 'nuget', - confidence: 0.9, - type: 'primary' - }); - logger.debug(`Found .NET project file: ${file}`); + for (const entry of entries) { + if (entry.isFile()) { + const ext = path.extname(entry.name); + if (['.csproj', '.fsproj', '.vbproj', '.sln'].includes(ext)) { + const filePath = path.join(dirPath, entry.name); + detectedFiles.push({ + path: filePath, + ecosystem: 'nuget', + confidence: ext === '.sln' ? 0.8 : 0.9, + type: 'primary' + }); + logger.debug(`Found .NET project file: ${entry.name}`); + } + } else if (entry.isDirectory() && !IGNORE_DIRECTORIES.includes(entry.name)) { + await this.scanForDotNetFiles( + path.join(dirPath, entry.name), + detectedFiles, + depth + 1 + ); } } } catch (error) { - logger.debug('Error scanning for .NET projects:', error); + logger.debug(`Error scanning directory for .NET files: ${dirPath}`, error); } } /** - * Detect ecosystem by file content + * Detect ecosystem by file content with improved patterns */ private async detectByContent(filePath: string): Promise { try { const content = await fs.readFile(filePath, 'utf-8'); const fileName = path.basename(filePath); - // Content-based detection patterns + // Enhanced content-based detection patterns const patterns = [ - { ecosystem: 'npm', pattern: /"dependencies":|"devDependencies":|"scripts":/, confidence: 0.8 }, - { ecosystem: 'pypi', pattern: /^[a-zA-Z0-9\-_.]+[>=<~!]=/, confidence: 0.7 }, - { ecosystem: 'maven', pattern: /||/, confidence: 0.8 }, - { ecosystem: 'nuget', pattern: /=<~!]=|^-r\s+|^--requirement\s+|^pip\s+install/, + confidence: 0.7 + }, + { + ecosystem: 'maven', + pattern: /|||.*<\/version>| { @@ -315,9 +520,18 @@ export class DependencyDetector { if (b.type === 'primary' && a.type !== 'primary') return 1; // Then by confidence - if (a.confidence !== b.confidence) return b.confidence - a.confidence; + if (Math.abs(a.confidence - b.confidence) > 0.05) { + return b.confidence - a.confidence; + } + + // Prefer files closer to project root + const aDepth = a.path.split(path.sep).length; + const bDepth = b.path.split(path.sep).length; + if (aDepth !== bDepth) { + return aDepth - bDepth; + } - // Then by ecosystem preference (npm, pypi, maven, etc.) + // Then by ecosystem preference const ecosystemPriority = ['npm', 'pypi', 'maven', 'nuget', 'rubygems', 'composer', 'go', 'cargo']; const aIndex = ecosystemPriority.indexOf(a.ecosystem); const bIndex = ecosystemPriority.indexOf(b.ecosystem); @@ -325,5 +539,27 @@ export class DependencyDetector { return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex); }); } + + /** + * Validate ecosystem compatibility + */ + validateEcosystem(ecosystem: string): boolean { + return ECOSYSTEM_CONFIGS.some(config => config.name === ecosystem); + } + + /** + * Get supported ecosystems + */ + getSupportedEcosystems(): string[] { + return ECOSYSTEM_CONFIGS.map(config => config.name); + } + + /** + * Get ecosystem display name + */ + getEcosystemDisplayName(ecosystem: string): string { + const config = ECOSYSTEM_CONFIGS.find(c => c.name === ecosystem); + return config?.displayName || ecosystem; + } } diff --git a/src/services/mongoApi.ts b/src/services/mongoApi.ts new file mode 100644 index 0000000..dc17dbe --- /dev/null +++ b/src/services/mongoApi.ts @@ -0,0 +1,302 @@ +import axios, { AxiosInstance } from 'axios'; +import { + ApiResponse, + AnalysisRequest, + AutoAnalysisRequest, + ApiStats, + ApiInfo +} from '../types/api'; +import { config } from '../utils/config'; +import { logger } from '../utils/logger'; + +/** + * MongoDB API Client for direct vulnerability queries + * Optimized for high-performance analysis using MongoDB backend + */ +export class MongoApiClient { + private client: AxiosInstance; + private fallbackClient?: AxiosInstance; + + constructor() { + // Primary MongoDB API client + this.client = axios.create({ + baseURL: config.getMongoApiUrl() || config.getApiUrl(), + timeout: config.getTimeout(), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'vulnify-cli/1.0.2', + 'X-Client-Version': '2.0.0', + 'X-Source': 'vulnify-cli', + 'Accept-Encoding': 'identity' // Disable compression + }, + decompress: false, // Disable automatic decompression + responseType: 'json', + validateStatus: (status) => status < 500 // Don't throw on 4xx errors + }); + + // Fallback to external APIs if MongoDB service is unavailable + const fallbackUrl = config.getApiUrl(); + if (fallbackUrl && fallbackUrl !== config.getMongoApiUrl()) { + this.fallbackClient = axios.create({ + baseURL: fallbackUrl, + timeout: config.getTimeout(), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'vulnify-cli/1.0.2' + } + }); + + // Add API key for fallback if available + const apiKey = config.getApiKey(); + if (apiKey && this.fallbackClient) { + this.fallbackClient.defaults.params = { apiKey }; + } + } + + this.setupInterceptors(); + } + + private setupInterceptors() { + // MongoDB API interceptors + this.client.interceptors.request.use( + (config) => { + logger.debug(`API Request: ${config.method?.toUpperCase()} ${config.url}`); + return config; + }, + (error) => { + logger.error('API Request Error:', error.message); + return Promise.reject(error); + } + ); + + this.client.interceptors.response.use( + (response) => { + logger.debug(`API Response: ${response.status} ${response.statusText}`); + return response; + }, + (error) => { + if (error.response) { + logger.error(`API Error: ${error.response.status} ${error.response.statusText}`); + } else if (error.request) { + logger.error('API Network Error:', error.message); + } else { + logger.error('API Error:', error.message); + } + return Promise.reject(error); + } + ); + + // Fallback API interceptors + if (this.fallbackClient) { + this.fallbackClient.interceptors.request.use( + (config) => { + logger.debug(`Fallback API Request: ${config.method?.toUpperCase()} ${config.url}`); + return config; + }, + (error) => { + logger.error('Fallback API Request Error:', error.message); + return Promise.reject(error); + } + ); + + this.fallbackClient.interceptors.response.use( + (response) => { + logger.debug(`Fallback API Response: ${response.status} ${response.statusText}`); + return response; + }, + (error) => { + if (error.response) { + logger.error(`Fallback API Error: ${error.response.status} ${error.response.statusText}`); + } else { + logger.error('Fallback API Error:', error.message); + } + return Promise.reject(error); + } + ); + } + } + + /** + * Analyze dependencies using MongoDB backend with fallback + */ + async analyze(request: AnalysisRequest): Promise { + try { + const response = await this.client.post('/api/v1/analyze', request); + return response.data; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const statusCode = (error as any)?.response?.status; + const responseData = (error as any)?.response?.data; + + // Try fallback API if available + if (this.fallbackClient) { + try { + logger.info('Attempting fallback API', { + fallbackUrl: this.fallbackClient.defaults.baseURL + }); + + const response = await this.fallbackClient.post('/api/v1/analyze', request); + + logger.info('Fallback analysis completed', { + ecosystem: request.ecosystem, + dependencies: request.dependencies.length, + vulnerabilities: response.data.results?.vulnerabilities_found || 0, + source: 'fallback' + }); + + return response.data; + + } catch (fallbackError) { + const fallbackErrorMessage = fallbackError instanceof Error ? fallbackError.message : 'Unknown error'; + const fallbackStatusCode = (fallbackError as any)?.response?.status; + + logger.error('Both MongoDB and fallback APIs failed', { + mongoError: errorMessage, + mongoStatusCode: statusCode, + fallbackError: fallbackErrorMessage, + fallbackStatusCode: fallbackStatusCode + }); + + throw new Error('All vulnerability analysis services are unavailable'); + } + } + + throw error; + } + } + + /** + * Auto-analyze dependencies from file content + */ + async autoAnalyze(request: AutoAnalysisRequest): Promise { + try { + + const response = await this.client.post('/api/v1/analyze/auto', request); + + + return response.data; + + } catch (error) { + logger.warn('Auto-analysis failed, attempting fallback', { + error: error instanceof Error ? error.message : 'Unknown error', + ecosystem: request.ecosystem + }); + + // Try fallback API if available + if (this.fallbackClient) { + try { + const response = await this.fallbackClient.post('/api/v1/analyze/auto', request); + + logger.info('Fallback auto-analysis completed', { + ecosystem: request.ecosystem, + dependencies: response.data.results?.total_dependencies || 0, + vulnerabilities: response.data.results?.vulnerabilities_found || 0, + source: 'fallback' + }); + + return response.data; + + } catch (fallbackError) { + logger.error('Both MongoDB and fallback auto-analysis failed', { + mongoError: error instanceof Error ? error.message : 'Unknown error', + fallbackError: fallbackError instanceof Error ? fallbackError.message : 'Unknown error' + }); + throw new Error('All auto-analysis services are unavailable'); + } + } + + throw error; + } + } + +/** + * Get API information + */ +async getInfo(): Promise { + try { + const response = await this.client.request({ + url: 'https://api-dev.vulnify.io/api/v1/info', + method: 'GET', + data: {} // forΓ§a envio de JSON vazio + }); + return response.data; + } catch (error) { + throw error; + } +} + +/** + * Get API statistics and health information + */ +async getStats(): Promise { + try { + const response = await this.client.request({ + url: '/stats', + method: 'GET', + data: {} // forΓ§a envio de JSON vazio + }); + return response.data; + } catch (error) { + throw error; + } +} + +/** + * Health check + */ +async healthCheck(): Promise<{ status: string; timestamp: string }> { + try { + const response = await this.client.request({ + url: '/health', + method: 'GET', + data: {} // forΓ§a envio de JSON vazio + }); + return response.data; + } catch (error) { + throw error; + } +} + + + /** + * Test connectivity to both MongoDB and fallback APIs + */ + async testConnectivity(): Promise<{ + mongodb: { available: boolean; responseTime?: number; error?: string }; + fallback: { available: boolean; responseTime?: number; error?: string }; + }> { + const result = { + mongodb: { available: false, responseTime: undefined as number | undefined, error: undefined as string | undefined }, + fallback: { available: false, responseTime: undefined as number | undefined, error: undefined as string | undefined } + }; + + // Test MongoDB API + try { + const start = Date.now(); + await this.client.get('/api/v1/health', { timeout: 5000 }); + result.mongodb.available = true; + result.mongodb.responseTime = Date.now() - start; + } catch (error) { + result.mongodb.error = error instanceof Error ? error.message : 'Unknown error'; + } + + // Test fallback API + if (this.fallbackClient) { + try { + const start = Date.now(); + await this.fallbackClient.get('/api/v1/health', { timeout: 5000 }); + result.fallback.available = true; + result.fallback.responseTime = Date.now() - start; + } catch (error) { + result.fallback.error = error instanceof Error ? error.message : 'Unknown error'; + } + } + + return result; + } +} + +// Export singleton instance +export const mongoApiClient = new MongoApiClient(); + diff --git a/src/services/reporter.ts b/src/services/reporter.ts index c7a1473..57a2ed1 100644 --- a/src/services/reporter.ts +++ b/src/services/reporter.ts @@ -1,12 +1,100 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import Table from 'cli-table3'; -import { AnalysisResults } from '../types/api'; +import { AnalysisResults, DependencyAnalysis, Vulnerability } from '../types/api'; import { Report, ReportMetadata, Recommendation } from '../types/cli'; import { colors, formatSeverity, formatCount } from '../utils/colors'; import { logger } from '../utils/logger'; +export interface EnhancedDisplayOptions { + ecosystem?: string; + projectPath?: string; + verbose?: boolean; +} + +interface GroupedDependency { + name: string; + version: string; + vulnerabilities: Vulnerability[]; + totalVulnerabilities: number; + severityCounts: { + critical: number; + high: number; + medium: number; + low: number; + }; + highestSeverity: string; +} + export class ReportGenerator { + /** + * IMPROVED: Group dependencies to avoid duplicates and improve visualization + */ + private groupDependencies(dependencies: DependencyAnalysis[]): GroupedDependency[] { + const grouped = new Map(); + + for (const dep of dependencies) { + const key = `${dep.name}@${dep.version}`; + + if (grouped.has(key)) { + // Merge vulnerabilities for the same package + const existing = grouped.get(key)!; + + // Add new vulnerabilities, avoiding duplicates + const existingIds = new Set(existing.vulnerabilities.map(v => v.id)); + const newVulns = dep.vulnerabilities.filter(v => !existingIds.has(v.id)); + + existing.vulnerabilities.push(...newVulns); + existing.totalVulnerabilities = existing.vulnerabilities.length; + + // Recalculate severity counts + existing.severityCounts = this.calculateSeverityCounts(existing.vulnerabilities); + existing.highestSeverity = this.getHighestSeverity(existing.vulnerabilities.map(v => v.severity)); + } else { + // Create new grouped dependency + const severityCounts = this.calculateSeverityCounts(dep.vulnerabilities); + + grouped.set(key, { + name: dep.name, + version: dep.version, + vulnerabilities: [...dep.vulnerabilities], + totalVulnerabilities: dep.vulnerabilities.length, + severityCounts, + highestSeverity: this.getHighestSeverity(dep.vulnerabilities.map(v => v.severity)) + }); + } + } + + // Sort by highest severity first, then by vulnerability count + return Array.from(grouped.values()).sort((a, b) => { + const severityOrder = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 }; + const aSeverity = severityOrder[a.highestSeverity as keyof typeof severityOrder] || 0; + const bSeverity = severityOrder[b.highestSeverity as keyof typeof severityOrder] || 0; + + if (aSeverity !== bSeverity) { + return bSeverity - aSeverity; // Higher severity first + } + + return b.totalVulnerabilities - a.totalVulnerabilities; // More vulnerabilities first + }); + } + + /** + * Calculate severity counts for vulnerabilities + */ + private calculateSeverityCounts(vulnerabilities: Vulnerability[]) { + const counts = { critical: 0, high: 0, medium: 0, low: 0 }; + + for (const vuln of vulnerabilities) { + const severity = vuln.severity?.toLowerCase(); + if (severity && severity in counts) { + counts[severity as keyof typeof counts]++; + } + } + + return counts; + } + /** * Generate and save JSON report */ @@ -15,9 +103,12 @@ export class ReportGenerator { metadata: Partial, outputPath?: string ): Promise { + // Use grouped dependencies for the report + const groupedDeps = this.groupDependencies(results.dependencies); + const report: Report = { metadata: { - cli_version: '1.0.0', + cli_version: '1.0.2', scan_timestamp: new Date().toISOString(), request_id: results.metadata.request_id, project_path: process.cwd(), @@ -33,7 +124,7 @@ export class ReportGenerator { medium: results.summary.medium, low: results.summary.low }, - dependencies: results.dependencies.map(dep => ({ + dependencies: groupedDeps.map(dep => ({ name: dep.name, version: dep.version, vulnerabilities: dep.vulnerabilities.map(vuln => ({ @@ -60,7 +151,59 @@ export class ReportGenerator { } /** - * Display results in table format + * IMPROVED: Display results in enhanced format with better grouping + */ + displayEnhancedResults(results: AnalysisResults, options: EnhancedDisplayOptions = {}): void { + console.log(colors.title('πŸ” Vulnify CLI')); + console.log(''); + + // Project info + if (options.ecosystem && options.projectPath) { + console.log(`πŸ“¦ Found ${results.total_dependencies} dependencies (${options.ecosystem} ecosystem)`); + } else { + console.log(`πŸ“¦ Found ${results.total_dependencies} dependencies`); + } + + console.log(`πŸ” Analyzing vulnerabilities... (${results.scan_time})`); + + if (results.vulnerabilities_found > 0) { + const groupedDeps = this.groupDependencies(results.dependencies); + const affectedPackages = groupedDeps.filter(dep => dep.totalVulnerabilities > 0); + + console.log(`⚠️ Found ${results.vulnerabilities_found} vulnerabilities in ${affectedPackages.length} packages:`); + console.log(''); + + // Display summary by severity + const { summary } = results; + if (summary.critical > 0) console.log(`πŸ”΄ ${summary.critical} Critical`); + if (summary.high > 0) console.log(`🟠 ${summary.high} High`); + if (summary.medium > 0) console.log(`🟑 ${summary.medium} Medium`); + if (summary.low > 0) console.log(`🟒 ${summary.low} Low`); + + console.log(''); + console.log(colors.subtitle('πŸ“¦ Affected Packages:')); + + // Show top affected packages + const topPackages = affectedPackages.slice(0, 10); + for (const dep of topPackages) { + const emoji = this.getSeverityEmoji(dep.highestSeverity); + const severityText = formatSeverity(dep.highestSeverity); + console.log(`${emoji} ${colors.highlight(dep.name)}@${colors.muted(dep.version)} - ${dep.totalVulnerabilities} vulnerabilities (${severityText})`); + } + + if (affectedPackages.length > 10) { + console.log(colors.muted(`... and ${affectedPackages.length - 10} more packages`)); + } + } else { + console.log('βœ… No vulnerabilities found!'); + } + + console.log(''); + console.log(`πŸ“„ Report saved to vulnify-report.json`); + } + + /** + * IMPROVED: Display results in table format with better grouping */ displayTableResults(results: AnalysisResults): void { console.log(''); @@ -70,41 +213,87 @@ export class ReportGenerator { // Summary this.displaySummary(results); - // Vulnerabilities table - if (results.vulnerabilities_found > 0) { + // Group dependencies to avoid duplicates + const groupedDeps = this.groupDependencies(results.dependencies); + const affectedPackages = groupedDeps.filter(dep => dep.totalVulnerabilities > 0); + + if (affectedPackages.length > 0) { console.log(''); - console.log(colors.subtitle('πŸ” Detected Vulnerabilities:')); + console.log(colors.subtitle('πŸ” Affected Packages:')); console.log(''); - const table = new Table({ + // Package summary table + const packageTable = new Table({ head: [ colors.bold('Package'), colors.bold('Version'), - colors.bold('Vulnerability'), - colors.bold('Severity'), - colors.bold('CVSS'), - colors.bold('Fixed In') + colors.bold('Vulnerabilities'), + colors.bold('Highest Severity'), + colors.bold('Critical'), + colors.bold('High'), + colors.bold('Medium'), + colors.bold('Low') ], - colWidths: [20, 15, 40, 12, 8, 15], - wordWrap: true + colWidths: [25, 15, 15, 15, 10, 10, 10, 10] }); - for (const dep of results.dependencies) { - if (dep.vulnerabilities.length > 0) { - for (const vuln of dep.vulnerabilities) { - table.push([ + for (const dep of affectedPackages) { + packageTable.push([ + colors.highlight(dep.name), + colors.muted(dep.version), + colors.warning(dep.totalVulnerabilities.toString()), + formatSeverity(dep.highestSeverity), + dep.severityCounts.critical > 0 ? colors.error(dep.severityCounts.critical.toString()) : '0', + dep.severityCounts.high > 0 ? colors.warning(dep.severityCounts.high.toString()) : '0', + dep.severityCounts.medium > 0 ? colors.info(dep.severityCounts.medium.toString()) : '0', + dep.severityCounts.low > 0 ? colors.muted(dep.severityCounts.low.toString()) : '0' + ]); + } + + console.log(packageTable.toString()); + + // Detailed vulnerabilities table (only for critical and high) + const criticalAndHigh = affectedPackages.filter(dep => + dep.severityCounts.critical > 0 || dep.severityCounts.high > 0 + ); + + if (criticalAndHigh.length > 0) { + console.log(''); + console.log(colors.subtitle('🚨 Critical & High Severity Vulnerabilities:')); + console.log(''); + + const vulnTable = new Table({ + head: [ + colors.bold('Package'), + colors.bold('CVE/ID'), + colors.bold('Severity'), + colors.bold('CVSS'), + colors.bold('Fixed In'), + colors.bold('Description') + ], + colWidths: [20, 20, 12, 8, 15, 40], + wordWrap: true + }); + + for (const dep of criticalAndHigh) { + const criticalHighVulns = dep.vulnerabilities.filter(v => + v.severity === 'critical' || v.severity === 'high' + ); + + for (const vuln of criticalHighVulns) { + vulnTable.push([ colors.highlight(dep.name), - colors.muted(dep.version), - vuln.title, + vuln.id || 'N/A', formatSeverity(vuln.severity), (vuln.cvss_score && typeof vuln.cvss_score === 'number') ? vuln.cvss_score.toFixed(1) : 'N/A', - vuln.fixed_in ? vuln.fixed_in.join(', ') : 'N/A' + vuln.fixed_in && vuln.fixed_in.length > 0 ? vuln.fixed_in.join(', ') : colors.muted('No fix available'), + this.truncateDescription(vuln.description || 'No description available', 100) ]); } } - } - console.log(table.toString()); + console.log(vulnTable.toString()); + } } // Recommendations @@ -120,11 +309,22 @@ export class ReportGenerator { * Display results in JSON format */ displayJsonResults(results: AnalysisResults): void { - console.log(JSON.stringify(results, null, 2)); + // Use grouped dependencies for JSON output too + const groupedDeps = this.groupDependencies(results.dependencies); + const modifiedResults = { + ...results, + dependencies: groupedDeps.map(dep => ({ + name: dep.name, + version: dep.version, + vulnerabilities: dep.vulnerabilities + })) + }; + + console.log(JSON.stringify(modifiedResults, null, 2)); } /** - * Display results in summary format + * IMPROVED: Display results in summary format with better grouping */ displaySummaryResults(results: AnalysisResults): void { console.log(''); @@ -134,16 +334,20 @@ export class ReportGenerator { this.displaySummary(results); if (results.vulnerabilities_found > 0) { - console.log(''); - console.log(colors.subtitle('πŸ“¦ Affected Packages:')); + const groupedDeps = this.groupDependencies(results.dependencies); + const affectedPackages = groupedDeps.filter(dep => dep.totalVulnerabilities > 0); - const affectedPackages = results.dependencies.filter(dep => dep.vulnerabilities.length > 0); + console.log(''); + console.log(colors.subtitle(`πŸ“¦ ${affectedPackages.length} Affected Packages:`)); for (const dep of affectedPackages) { - const severities = dep.vulnerabilities.map(v => v.severity); - const highestSeverity = this.getHighestSeverity(severities); + const severityBreakdown = []; + if (dep.severityCounts.critical > 0) severityBreakdown.push(`${dep.severityCounts.critical} critical`); + if (dep.severityCounts.high > 0) severityBreakdown.push(`${dep.severityCounts.high} high`); + if (dep.severityCounts.medium > 0) severityBreakdown.push(`${dep.severityCounts.medium} medium`); + if (dep.severityCounts.low > 0) severityBreakdown.push(`${dep.severityCounts.low} low`); - console.log(` ${formatSeverity(highestSeverity)} ${colors.highlight(dep.name)}@${colors.muted(dep.version)} (${dep.vulnerabilities.length} vulnerabilities)`); + console.log(` ${formatSeverity(dep.highestSeverity)} ${colors.highlight(dep.name)}@${colors.muted(dep.version)} (${severityBreakdown.join(', ')})`); } } @@ -156,9 +360,12 @@ export class ReportGenerator { */ private displaySummary(results: AnalysisResults): void { const { summary } = results; + const groupedDeps = this.groupDependencies(results.dependencies); + const affectedPackages = groupedDeps.filter(dep => dep.totalVulnerabilities > 0); console.log(`${colors.info('πŸ“¦')} Total Dependencies: ${colors.highlight(results.total_dependencies.toString())}`); console.log(`${colors.error('🚨')} Vulnerabilities Found: ${colors.highlight(results.vulnerabilities_found.toString())}`); + console.log(`${colors.warning('πŸ“‹')} Affected Packages: ${colors.highlight(affectedPackages.length.toString())}`); console.log(''); if (results.vulnerabilities_found > 0) { @@ -176,7 +383,8 @@ export class ReportGenerator { * Display recommendations */ private displayRecommendations(results: AnalysisResults): void { - const recommendations = this.generateRecommendations(results); + const groupedDeps = this.groupDependencies(results.dependencies); + const recommendations = this.generateRecommendationsFromGrouped(groupedDeps); if (recommendations.length > 0) { console.log(''); @@ -188,7 +396,7 @@ export class ReportGenerator { case 'upgrade': console.log(` ${colors.success('β†—')} Upgrade ${colors.highlight(rec.dependency)} from ${colors.muted(rec.current_version)} to ${colors.success(rec.recommended_version || 'latest')}`); if (rec.fixes_vulnerabilities.length > 0) { - console.log(` Fixes: ${rec.fixes_vulnerabilities.join(', ')}`); + console.log(` Fixes: ${rec.fixes_vulnerabilities.slice(0, 3).join(', ')}${rec.fixes_vulnerabilities.length > 3 ? '...' : ''}`); } break; case 'patch': @@ -203,13 +411,13 @@ export class ReportGenerator { } /** - * Generate recommendations based on analysis results + * Generate recommendations based on grouped dependencies */ - private generateRecommendations(results: AnalysisResults): Recommendation[] { + private generateRecommendationsFromGrouped(groupedDeps: GroupedDependency[]): Recommendation[] { const recommendations: Recommendation[] = []; - for (const dep of results.dependencies) { - if (dep.vulnerabilities.length > 0) { + for (const dep of groupedDeps) { + if (dep.totalVulnerabilities > 0) { // Find the best version that fixes vulnerabilities const allFixedVersions = dep.vulnerabilities .flatMap(v => v.fixed_in || []) @@ -217,7 +425,7 @@ export class ReportGenerator { if (allFixedVersions.length > 0) { // Get the latest fixed version - const recommendedVersion = allFixedVersions[allFixedVersions.length - 1]; + const recommendedVersion = this.getLatestVersion(allFixedVersions); recommendations.push({ type: 'upgrade', @@ -228,9 +436,7 @@ export class ReportGenerator { }); } else { // No fixed version available - const hasCriticalOrHigh = dep.vulnerabilities.some(v => - v.severity === 'critical' || v.severity === 'high' - ); + const hasCriticalOrHigh = dep.severityCounts.critical > 0 || dep.severityCounts.high > 0; if (hasCriticalOrHigh) { recommendations.push({ @@ -248,6 +454,36 @@ export class ReportGenerator { return recommendations; } + /** + * Generate recommendations based on analysis results (legacy method) + */ + private generateRecommendations(results: AnalysisResults): Recommendation[] { + const groupedDeps = this.groupDependencies(results.dependencies); + return this.generateRecommendationsFromGrouped(groupedDeps); + } + + /** + * Get the latest version from a list of versions + */ + private getLatestVersion(versions: string[]): string { + // Simple heuristic: return the last version in the array + // In a real implementation, you might want to use semver comparison + return versions[versions.length - 1]; + } + + /** + * Get emoji for severity level + */ + private getSeverityEmoji(severity: string): string { + switch (severity.toLowerCase()) { + case 'critical': return 'πŸ”΄'; + case 'high': return '🟠'; + case 'medium': return '🟑'; + case 'low': return '🟒'; + default: return 'βšͺ'; + } + } + /** * Get the highest severity from a list of severities */ @@ -262,5 +498,16 @@ export class ReportGenerator { return 'low'; } + + /** + * Truncate description to specified length + */ + private truncateDescription(description: string, maxLength: number): string { + if (description.length <= maxLength) { + return description; + } + + return description.substring(0, maxLength - 3) + '...'; + } } diff --git a/src/types/api.ts b/src/types/api.ts index 4084973..ffff3d3 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -58,7 +58,8 @@ export interface AnalysisRequest { } export interface AutoAnalysisRequest { - content: string; + ecosystem: string; + file_content: string; filename?: string; } diff --git a/src/types/cli.ts b/src/types/cli.ts index 85d4167..617b379 100644 --- a/src/types/cli.ts +++ b/src/types/cli.ts @@ -1,7 +1,7 @@ export interface CliOptions { file?: string; ecosystem?: string; - output?: 'json' | 'table' | 'summary'; + output?: 'json' | 'table' | 'summary' | 'enhanced'; severity?: 'critical' | 'high' | 'medium' | 'low'; apiKey?: string; timeout?: number; @@ -100,3 +100,28 @@ export interface EcosystemConfig { parser: string; } + +export interface ProjectStructure { + isMonorepo: boolean; + rootEcosystem: string | null; + subprojects: Array<{ + path: string; + ecosystem: string; + files: number; + }>; + totalFiles: number; +} + +export interface ValidationResult { + valid: boolean; + message?: string; + suggestions?: string[]; +} + +export interface EnhancedCliOptions extends CliOptions { + verbose?: boolean; + maxDepth?: number; + includeSubdirs?: boolean; + format?: 'enhanced' | 'table' | 'json' | 'summary'; +} + diff --git a/src/utils/config.ts b/src/utils/config.ts index c449486..83b8ff4 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -3,7 +3,8 @@ import * as path from 'path'; import { Config } from '../types'; const DEFAULT_CONFIG: Config = { - api_url: 'https://api-dev.vulnify.io/api/v1', + api_url: 'https://api-dev.vulnify.io', + // opcional: mongo_api_url: undefined, timeout: 30000, severity_threshold: 'medium', output_format: 'table', @@ -19,32 +20,40 @@ export class ConfigManager { this.loadConfig(); } + // βœ… implementar com tipos explΓ­citos + public getApiKey(): string | undefined { + return this.config.api_key; + } + + // βœ… implementar com tipos explΓ­citos + public getMongoApiUrl(): string | undefined { + // suporta chave vinda do rc ou do env + return this.config.api_url; + } + private loadConfig(): void { - // Try to load from project directory first const projectConfigPath = path.join(process.cwd(), '.vulnifyrc'); if (fs.existsSync(projectConfigPath)) { try { const projectConfig = fs.readJsonSync(projectConfigPath); this.config = { ...this.config, ...projectConfig }; return; - } catch (error) { + } catch { console.warn('Warning: Invalid .vulnifyrc in project directory'); } } - // Try to load from home directory const homeDir = process.env.HOME || process.env.USERPROFILE || ''; const homeConfigPath = path.join(homeDir, '.vulnifyrc'); if (fs.existsSync(homeConfigPath)) { try { const homeConfig = fs.readJsonSync(homeConfigPath); this.config = { ...this.config, ...homeConfig }; - } catch (error) { + } catch { console.warn('Warning: Invalid .vulnifyrc in home directory'); } } - // Load from environment variables this.loadFromEnv(); } @@ -57,11 +66,14 @@ export class ConfigManager { this.config.api_url = process.env.VULNIFY_API_URL; } + // βœ… opcional: url dedicada para o serviΓ§o Mongo + if (process.env.VULNIFY_MONGO_API_URL) { + (this.config as any).mongo_api_url = process.env.VULNIFY_MONGO_API_URL; + } + if (process.env.VULNIFY_TIMEOUT) { const timeout = parseInt(process.env.VULNIFY_TIMEOUT, 10); - if (!isNaN(timeout)) { - this.config.timeout = timeout; - } + if (!isNaN(timeout)) this.config.timeout = timeout; } if (process.env.VULNIFY_OUTPUT) { @@ -84,10 +96,6 @@ export class ConfigManager { return this.config.api_url; } - public getApiKey(): string | undefined { - return this.config.api_key; - } - public getTimeout(): number { return this.config.timeout; } @@ -102,4 +110,3 @@ export class ConfigManager { } export const config = new ConfigManager(); -