UNPKG

@profullstack/scanner

Version:

A comprehensive CLI and Node.js module for web application security scanning with OWASP compliance, supporting multiple scanning tools and detailed vulnerability reporting

1,139 lines (980 loc) โ€ข 42.8 kB
#!/usr/bin/env node import { Command } from 'commander'; import prompts from 'prompts'; import chalk from 'chalk'; import ora from 'ora'; import { exec } from 'child_process'; import { promisify } from 'util'; import { scanTarget, getScanHistory, getAllScans, getScanStats, getScanById, deleteScan, clearScanHistory } from '../lib/scanner.js'; import { checkToolAvailability, getInstallationInstructions } from '../lib/tools.js'; import { generateReport, exportReport, getReportFormats } from '../lib/reports.js'; import { validateTarget, formatDuration, getSeverityEmoji, getSeverityColor } from '../lib/utils.js'; import { getConfig, updateConfig, resetConfig, getEnabledTools, setToolEnabled, getScanProfiles, applyScanProfile, getConfigFilePath, exportConfig, importConfig, addProject, removeProject, getProjects, getProject, updateProject, getProjectHistory, getAllHistory, clearProjectHistory, getProjectStats } from '../lib/config.js'; const program = new Command(); // Helper function to create spinner function createSpinner(text) { return ora({ text, spinner: 'dots', color: 'cyan' }); } // Helper function to display vulnerability summary function displayVulnerabilitySummary(summary) { console.log('\n๐Ÿ“Š Vulnerability Summary:'); console.log(`${getSeverityEmoji('critical')} Critical: ${chalk.red(summary.critical)}`); console.log(`${getSeverityEmoji('high')} High: ${chalk.redBright(summary.high)}`); console.log(`${getSeverityEmoji('medium')} Medium: ${chalk.yellow(summary.medium)}`); console.log(`${getSeverityEmoji('low')} Low: ${chalk.cyan(summary.low)}`); console.log(`${getSeverityEmoji('info')} Info: ${chalk.gray(summary.info)}`); console.log(`๐Ÿ“ˆ Total: ${chalk.bold(summary.total)}`); } // Helper function to open a file in the default browser async function openInBrowser(filePath) { const execAsync = promisify(exec); const platform = process.platform; let command; try { if (platform === 'win32') { command = `start "" "${filePath}"`; } else if (platform === 'darwin') { command = `open "${filePath}"`; } else { // Linux command = `xdg-open "${filePath}"`; } await execAsync(command); return true; } catch (error) { console.error(chalk.red(`Failed to open file in browser: ${error.message}`)); return false; } } // Main scan command program .command('scan <target>') .description('Scan a target URL or IP address for security vulnerabilities\n' + ' <target> can be:\n' + ' - Full URL: https://example.com\n' + ' - Domain: example.com\n' + ' - IP address: 192.168.1.1\n' + ' - URL with path: https://example.com/app') .option('-t, --tools <tools>', 'Comma-separated list of tools to use (nikto,zap,wapiti,nuclei,sqlmap)', '') .option('-o, --output <dir>', 'Output directory for scan results') .option('-f, --format <format>', 'Report format (json,html,csv,xml,markdown,text)', 'json') .option('--multi-format', 'Generate reports in multiple formats (comma-separated in --format)') .option('--ui-json', 'Generate UI-friendly JSON with additional metadata') .option('--open-html', 'Open HTML report in browser after generation') .option('-p, --profile <profile>', 'Use predefined scan profile (quick,standard,comprehensive,owasp)') .option('--project <project>', 'Project ID or name to associate scan with') .option('--timeout <seconds>', 'Timeout for each tool in seconds', '300') .option('--verbose', 'Enable verbose output') .option('--no-report', 'Skip report generation') .option('--auth-user <username>', 'Username for HTTP authentication') .option('--auth-pass <password>', 'Password for HTTP authentication') .option('--auth-type <type>', 'Authentication type (basic,digest,form)', 'basic') .option('--login-url <url>', 'Login URL for form-based authentication') .option('--login-data <data>', 'Login form data (e.g., "username=admin&password=secret")') .option('--session-cookie <cookie>', 'Session cookie for authenticated scanning') .option('--headers <headers>', 'Custom HTTP headers (JSON format)') .action(async (target, options) => { try { // Validate target const validation = validateTarget(target); if (!validation.valid) { console.error(chalk.red(`โŒ ${validation.error}`)); process.exit(1); } console.log(chalk.blue('๐Ÿ›ก๏ธ Security Scanner')); console.log(chalk.gray(`Target: ${target}`)); // Validate project if specified let project = null; if (options.project) { project = getProject(options.project); if (!project) { console.error(chalk.red(`โŒ Project not found: ${options.project}`)); process.exit(1); } console.log(chalk.cyan(`๐Ÿ“ Project: ${project.name}`)); } // Determine tools to use let tools = []; let toolOptions = {}; if (options.profile) { try { const profileConfig = applyScanProfile(options.profile); tools = profileConfig.tools; toolOptions = profileConfig.toolOptions; console.log(chalk.cyan(`๐Ÿ“‹ Using profile: ${options.profile}`)); } catch (error) { console.error(chalk.red(`โŒ ${error.message}`)); const profiles = Object.keys(getScanProfiles()); console.log(chalk.yellow(`Available profiles: ${profiles.join(', ')}`)); process.exit(1); } } else if (options.tools) { tools = options.tools.split(',').map(t => t.trim()); } else { tools = getEnabledTools(); } if (tools.length === 0) { console.error(chalk.red('โŒ No tools specified or enabled')); console.log(chalk.yellow('๐Ÿ’ก Use --tools to specify tools or --profile to use a predefined profile')); process.exit(1); } // Check tool availability const spinner = createSpinner('Checking tool availability...'); spinner.start(); const availability = await checkToolAvailability(tools); const availableTools = tools.filter(tool => availability[tool]); const missingTools = tools.filter(tool => !availability[tool]); spinner.stop(); if (missingTools.length > 0) { console.log(chalk.yellow(`โš ๏ธ Missing tools: ${missingTools.join(', ')}`)); const instructions = getInstallationInstructions(missingTools); console.log(chalk.cyan('\n๐Ÿ’ก Installation instructions:')); Object.entries(instructions).forEach(([tool, info]) => { console.log(chalk.white(`\n${tool.toUpperCase()}:`)); console.log(chalk.gray(` Description: ${info.description}`)); console.log(chalk.gray(` Ubuntu/Debian: ${info.ubuntu || 'N/A'}`)); console.log(chalk.gray(` CentOS/RHEL: ${info.centos || 'N/A'}`)); console.log(chalk.gray(` macOS: ${info.macos || 'N/A'}`)); if (info.note) { console.log(chalk.yellow(` Note: ${info.note}`)); } }); if (availableTools.length === 0) { console.error(chalk.red('\nโŒ No tools are available. Please install at least one security tool.')); process.exit(1); } const { proceed } = await prompts({ type: 'confirm', name: 'proceed', message: `Continue with available tools (${availableTools.join(', ')})?`, initial: true }); if (!proceed) { console.log(chalk.yellow('Scan cancelled.')); process.exit(0); } } console.log(chalk.green(`โœ… Using tools: ${availableTools.join(', ')}`)); // Prepare authentication options const authOptions = {}; if (options.authUser && options.authPass) { authOptions.type = options.authType || 'basic'; authOptions.username = options.authUser; authOptions.password = options.authPass; if (options.authType === 'form') { authOptions.loginUrl = options.loginUrl; authOptions.loginData = options.loginData; } console.log(chalk.cyan(`๐Ÿ” Using ${authOptions.type} authentication for user: ${authOptions.username}`)); } if (options.sessionCookie) { authOptions.sessionCookie = options.sessionCookie; console.log(chalk.cyan('๐Ÿช Using session cookie for authentication')); } // Parse custom headers let customHeaders = {}; if (options.headers) { try { customHeaders = JSON.parse(options.headers); console.log(chalk.cyan(`๐Ÿ“‹ Using custom headers: ${Object.keys(customHeaders).join(', ')}`)); } catch (error) { console.error(chalk.red(`โŒ Invalid headers JSON: ${error.message}`)); process.exit(1); } } // Prepare scan options const scanOptions = { tools: availableTools, outputDir: options.output, timeout: parseInt(options.timeout), verbose: options.verbose, toolOptions: toolOptions, auth: Object.keys(authOptions).length > 0 ? authOptions : null, headers: customHeaders, projectId: project?.id || null, scanProfile: options.profile || null }; // Start scan let scanSpinner = null; if (!options.verbose) { scanSpinner = createSpinner(`Scanning ${target}...`); scanSpinner.start(); } else { console.log(chalk.blue(`๐Ÿš€ Starting scan of ${target}...`)); } const startTime = Date.now(); const result = await scanTarget(target, scanOptions); const duration = Math.round((Date.now() - startTime) / 1000); if (scanSpinner) { scanSpinner.succeed(chalk.green(`Scan completed in ${formatDuration(duration)}`)); } else { console.log(chalk.green(`\nโœ… Scan completed in ${formatDuration(duration)}`)); } // Display results displayVulnerabilitySummary(result.summary); if (result.vulnerabilities.length > 0) { console.log(chalk.red(`\n๐Ÿšจ Found ${result.vulnerabilities.length} vulnerabilities:`)); result.vulnerabilities.slice(0, 5).forEach((vuln, index) => { const emoji = getSeverityEmoji(vuln.severity); const color = vuln.severity === 'critical' ? 'red' : vuln.severity === 'high' ? 'redBright' : vuln.severity === 'medium' ? 'yellow' : vuln.severity === 'low' ? 'cyan' : 'gray'; console.log(`${emoji} ${chalk[color](vuln.title || 'Unknown')} (${vuln.source})`); if (vuln.url) { console.log(chalk.gray(` URL: ${vuln.url}`)); } }); if (result.vulnerabilities.length > 5) { console.log(chalk.gray(` ... and ${result.vulnerabilities.length - 5} more`)); } } else { console.log(chalk.green('\nโœ… No vulnerabilities found!')); } // Generate report if (!options.noReport) { const reportSpinner = createSpinner('Generating report...'); reportSpinner.start(); try { // Handle multiple formats if (options.multiFormat) { const formats = options.format.split(',').map(f => f.trim()); reportSpinner.text = `Generating reports in formats: ${formats.join(', ')}...`; const reportResults = await exportReport(result, `${result.outputDir}/report`, { format: formats, multiFormat: true, outputDir: result.outputDir, uiFormat: options.uiJson }); reportSpinner.succeed(chalk.green(`Reports generated in ${formats.length} formats`)); // List all generated reports reportResults.forEach(report => { console.log(chalk.cyan(` - ${report.format}: ${report.filePath}`)); }); // Open HTML report if requested and available if (options.openHtml) { const htmlReport = reportResults.find(r => r.format === 'html'); if (htmlReport) { console.log(chalk.blue('\n๐ŸŒ Opening HTML report in browser...')); await openInBrowser(htmlReport.filePath); } } } else { // Single format const reportPath = `${result.outputDir}/report.${options.format}`; await exportReport(result, reportPath, { format: options.format, uiFormat: options.format === 'json' && options.uiJson }); reportSpinner.succeed(chalk.green(`Report saved: ${reportPath}`)); // Open HTML report in browser if requested if (options.openHtml && options.format === 'html') { console.log(chalk.blue('\n๐ŸŒ Opening HTML report in browser...')); await openInBrowser(reportPath); } } } catch (error) { reportSpinner.fail(chalk.red(`Report generation failed: ${error.message}`)); } } console.log(chalk.cyan(`\n๐Ÿ“ Results saved to: ${result.outputDir}`)); console.log(chalk.gray(`Scan ID: ${result.id}`)); } catch (error) { console.error(chalk.red(`โŒ Scan failed: ${error.message}`)); if (options.verbose) { console.error(error.stack); } process.exit(1); } }); // List scan history program .command('history') .description('Show scan history') .option('-l, --limit <number>', 'Number of scans to show', '10') .option('--all', 'Show all scans') .action(async (options) => { try { const scans = options.all ? getAllScans() : getScanHistory(parseInt(options.limit)); if (scans.length === 0) { console.log(chalk.yellow('No scan history found.')); return; } console.log(chalk.blue('๐Ÿ“‹ Scan History\n')); scans.forEach((scan, index) => { const status = scan.status === 'completed' ? chalk.green('โœ…') : scan.status === 'failed' ? chalk.red('โŒ') : chalk.yellow('โณ'); console.log(`${status} ${chalk.bold(scan.target)}`); console.log(chalk.gray(` ID: ${scan.id}`)); console.log(chalk.gray(` Date: ${new Date(scan.startTime).toLocaleString()}`)); console.log(chalk.gray(` Duration: ${formatDuration(scan.duration || 0)}`)); console.log(chalk.gray(` Tools: ${scan.tools.join(', ')}`)); console.log(chalk.gray(` Vulnerabilities: ${scan.summary?.total || 0}`)); if (index < scans.length - 1) { console.log(); } }); } catch (error) { console.error(chalk.red(`โŒ Error loading history: ${error.message}`)); process.exit(1); } }); // Show scan statistics program .command('stats') .description('Show scan statistics') .action(async () => { try { const stats = getScanStats(); console.log(chalk.blue('๐Ÿ“Š Scan Statistics\n')); console.log(chalk.bold('Overview:')); console.log(`Total Scans: ${chalk.cyan(stats.totalScans)}`); console.log(`Completed: ${chalk.green(stats.completedScans)}`); console.log(`Failed: ${chalk.red(stats.failedScans)}`); console.log(`Average Duration: ${chalk.yellow(formatDuration(stats.averageScanTime))}`); console.log(chalk.bold('\nVulnerabilities:')); console.log(`Total Found: ${chalk.cyan(stats.totalVulnerabilities)}`); displayVulnerabilitySummary(stats.severityBreakdown); if (stats.mostScannedTargets.length > 0) { console.log(chalk.bold('\nMost Scanned Targets:')); stats.mostScannedTargets.slice(0, 5).forEach(({ target, count }) => { console.log(`${chalk.cyan(target)}: ${count} scans`); }); } if (Object.keys(stats.toolUsage).length > 0) { console.log(chalk.bold('\nTool Usage:')); Object.entries(stats.toolUsage) .sort(([,a], [,b]) => b - a) .forEach(([tool, count]) => { console.log(`${chalk.cyan(tool)}: ${count} times`); }); } } catch (error) { console.error(chalk.red(`โŒ Error loading statistics: ${error.message}`)); process.exit(1); } }); // Show scan details program .command('show <scanId>') .description('Show detailed scan results') .option('-f, --format <format>', 'Output format (json,text,html,markdown,csv,xml)', 'text') .option('--ui-json', 'Use UI-friendly JSON format (only with json format)') .option('--detailed', 'Show detailed information in text format') .option('--no-color', 'Disable colored output in text format') .option('--open-html', 'Open HTML report in browser (only with html format or when saving to file)') .option('-o, --output <file>', 'Save output to file instead of displaying') .action(async (scanId, options) => { try { const scan = getScanById(scanId); if (!scan) { console.error(chalk.red(`โŒ Scan not found: ${scanId}`)); process.exit(1); } // Generate report in requested format const reportOptions = { format: options.format, uiFormat: options.uiJson && options.format === 'json', detailed: options.detailed, colorOutput: options.color }; try { // Generate the report const report = await generateReport(scan, reportOptions); // If output file is specified, save to file if (options.output) { const spinner = createSpinner(`Saving ${options.format} report to ${options.output}...`); spinner.start(); await exportReport(scan, options.output, reportOptions); spinner.succeed(chalk.green(`Report saved to ${options.output}`)); // Open HTML report in browser if requested if (options.openHtml && options.format === 'html') { console.log(chalk.blue('\n๐ŸŒ Opening HTML report in browser...')); await openInBrowser(options.output); } } else { // Display to console if (options.format === 'json') { console.log(typeof report === 'string' ? report : JSON.stringify(report, null, 2)); } else if (options.format === 'text') { console.log(report); } else { console.log(chalk.yellow(`\nโš ๏ธ ${options.format.toUpperCase()} format is better viewed in a file.`)); console.log(chalk.cyan(`๐Ÿ’ก Use --output option to save to a file instead of displaying.`)); console.log('\n' + report.substring(0, 500) + '...\n'); console.log(chalk.gray(`(Output truncated. Full report is ${report.length} characters)`)); } } } catch (error) { console.error(chalk.red(`โŒ Error generating report: ${error.message}`)); } } catch (error) { console.error(chalk.red(`โŒ Error showing scan: ${error.message}`)); process.exit(1); } }); // Generate report from existing scan program .command('report <scanId>') .description('Generate report from existing scan') .option('-f, --format <format>', 'Report format (json,html,csv,xml,markdown,text)', 'html') .option('--multi-format', 'Generate reports in multiple formats (comma-separated in --format)') .option('--ui-json', 'Generate UI-friendly JSON with additional metadata') .option('--open-html', 'Open HTML report in browser after generation') .option('-o, --output <file>', 'Output file path') .option('-d, --output-dir <dir>', 'Output directory for multiple formats') .option('--detailed', 'Include detailed information in text reports') .action(async (scanId, options) => { try { const scan = getScanById(scanId); if (!scan) { console.error(chalk.red(`โŒ Scan not found: ${scanId}`)); process.exit(1); } const spinner = createSpinner('Generating report...'); spinner.start(); try { if (options.multiFormat) { // Handle multiple formats const formats = options.format.split(',').map(f => f.trim()); spinner.text = `Generating reports in formats: ${formats.join(', ')}...`; const outputDir = options.outputDir || '.'; const baseFilename = options.output ? options.output.replace(/\.[^/.]+$/, '') : `report-${scanId}`; const reportResults = await exportReport(scan, `${outputDir}/${baseFilename}`, { format: formats, multiFormat: true, outputDir: outputDir, uiFormat: options.uiJson, detailed: options.detailed }); spinner.succeed(chalk.green(`Reports generated in ${formats.length} formats`)); // List all generated reports reportResults.forEach(report => { console.log(chalk.cyan(` - ${report.format}: ${report.filePath}`)); }); // Open HTML report if requested and available if (options.openHtml) { const htmlReport = reportResults.find(r => r.format === 'html'); if (htmlReport) { console.log(chalk.blue('\n๐ŸŒ Opening HTML report in browser...')); await openInBrowser(htmlReport.filePath); } } } else { // Single format const outputFile = options.output || `report-${scanId}.${options.format}`; await exportReport(scan, outputFile, { format: options.format, uiFormat: options.format === 'json' && options.uiJson, detailed: options.detailed }); spinner.succeed(chalk.green(`Report generated: ${outputFile}`)); // Open HTML report in browser if requested if (options.openHtml && options.format === 'html') { console.log(chalk.blue('\n๐ŸŒ Opening HTML report in browser...')); await openInBrowser(outputFile); } } } catch (error) { spinner.fail(chalk.red(`Report generation failed: ${error.message}`)); if (options.verbose) { console.error(error.stack); } } } catch (error) { console.error(chalk.red(`โŒ Error accessing scan: ${error.message}`)); process.exit(1); } }); // Tool management program .command('tools') .description('Manage security tools') .option('--check', 'Check tool availability') .option('--enable <tool>', 'Enable a tool') .option('--disable <tool>', 'Disable a tool') .option('--list', 'List all tools and their status') .action(async (options) => { try { if (options.check) { const spinner = createSpinner('Checking tool availability...'); spinner.start(); const tools = ['nikto', 'zap-cli', 'wapiti', 'nuclei', 'sqlmap']; const availability = await checkToolAvailability(tools); spinner.stop(); console.log(chalk.blue('๐Ÿ”ง Tool Availability\n')); tools.forEach(tool => { const status = availability[tool] ? chalk.green('โœ… Available') : chalk.red('โŒ Not found'); console.log(`${tool.toUpperCase()}: ${status}`); }); const missingTools = tools.filter(tool => !availability[tool]); if (missingTools.length > 0) { const instructions = getInstallationInstructions(missingTools); console.log(chalk.cyan('\n๐Ÿ’ก Installation instructions for missing tools:')); Object.entries(instructions).forEach(([tool, info]) => { console.log(chalk.white(`\n${tool.toUpperCase()}:`)); console.log(chalk.gray(` ${info.description}`)); console.log(chalk.gray(` Ubuntu: ${info.ubuntu || 'N/A'}`)); console.log(chalk.gray(` macOS: ${info.macos || 'N/A'}`)); }); } return; } if (options.enable) { setToolEnabled(options.enable, true); console.log(chalk.green(`โœ… Enabled tool: ${options.enable}`)); return; } if (options.disable) { setToolEnabled(options.disable, false); console.log(chalk.yellow(`โš ๏ธ Disabled tool: ${options.disable}`)); return; } if (options.list) { const config = getConfig(); console.log(chalk.blue('๐Ÿ”ง Tool Configuration\n')); Object.entries(config.tools).forEach(([tool, toolConfig]) => { const status = toolConfig.enabled ? chalk.green('โœ… Enabled') : chalk.red('โŒ Disabled'); console.log(`${tool.toUpperCase()}: ${status}`); console.log(chalk.gray(` Timeout: ${toolConfig.timeout}s`)); if (tool === 'nuclei' && toolConfig.severity) { console.log(chalk.gray(` Severity: ${toolConfig.severity}`)); } if (tool === 'sqlmap' && toolConfig.crawl) { console.log(chalk.gray(` Crawl depth: ${toolConfig.crawl}`)); } console.log(); }); return; } // Default: show help console.log(chalk.blue('๐Ÿ”ง Tool Management\n')); console.log('Available commands:'); console.log(' --check Check tool availability'); console.log(' --list List all tools and their status'); console.log(' --enable <tool> Enable a specific tool'); console.log(' --disable <tool> Disable a specific tool'); } catch (error) { console.error(chalk.red(`โŒ Tool management error: ${error.message}`)); process.exit(1); } }); // Configuration management program .command('config') .description('Manage scanner configuration') .option('--show', 'Show current configuration') .option('--reset', 'Reset configuration to defaults') .option('--export <file>', 'Export configuration to file') .option('--import <file>', 'Import configuration from file') .option('--profiles', 'Show available scan profiles') .action(async (options) => { try { if (options.show) { const config = getConfig(); console.log(JSON.stringify(config, null, 2)); return; } if (options.reset) { const { confirm } = await prompts({ type: 'confirm', name: 'confirm', message: 'Reset configuration to defaults?', initial: false }); if (confirm) { resetConfig(); console.log(chalk.green('โœ… Configuration reset to defaults')); } else { console.log(chalk.yellow('Reset cancelled')); } return; } if (options.export) { exportConfig(options.export); console.log(chalk.green(`โœ… Configuration exported to: ${options.export}`)); return; } if (options.import) { importConfig(options.import); console.log(chalk.green(`โœ… Configuration imported from: ${options.import}`)); return; } if (options.profiles) { const profiles = getScanProfiles(); console.log(chalk.blue('๐Ÿ“‹ Available Scan Profiles\n')); Object.entries(profiles).forEach(([name, profile]) => { console.log(chalk.bold(name.toUpperCase())); console.log(chalk.gray(` ${profile.description}`)); console.log(chalk.cyan(` Tools: ${profile.tools.join(', ')}`)); console.log(); }); return; } // Default: show config file location console.log(chalk.blue('โš™๏ธ Configuration\n')); console.log(`Config file: ${chalk.cyan(getConfigFilePath())}`); console.log('\nAvailable commands:'); console.log(' --show Show current configuration'); console.log(' --reset Reset to defaults'); console.log(' --export <file> Export configuration'); console.log(' --import <file> Import configuration'); console.log(' --profiles Show scan profiles'); } catch (error) { console.error(chalk.red(`โŒ Configuration error: ${error.message}`)); process.exit(1); } }); // Clean data program .command('clean') .description('Clean scan data and history') .option('--history', 'Clear scan history only') .option('--all', 'Clear all data including configuration') .action(async (options) => { try { if (options.history) { const { confirm } = await prompts({ type: 'confirm', name: 'confirm', message: 'Clear all scan history?', initial: false }); if (confirm) { clearScanHistory(); console.log(chalk.green('โœ… Scan history cleared')); } else { console.log(chalk.yellow('Clean cancelled')); } return; } if (options.all) { const { confirm } = await prompts({ type: 'confirm', name: 'confirm', message: 'Clear ALL data including configuration? This cannot be undone.', initial: false }); if (confirm) { clearScanHistory(); resetConfig(); console.log(chalk.green('โœ… All data cleared')); } else { console.log(chalk.yellow('Clean cancelled')); } return; } // Default: show options console.log(chalk.blue('๐Ÿงน Clean Data\n')); console.log('Available options:'); console.log(' --history Clear scan history only'); console.log(' --all Clear all data including configuration'); } catch (error) { console.error(chalk.red(`โŒ Clean error: ${error.message}`)); process.exit(1); } }); // Delete specific scan program .command('delete <scanId>') .description('Delete a specific scan from history') .action(async (scanId) => { try { const scan = getScanById(scanId); if (!scan) { console.error(chalk.red(`โŒ Scan not found: ${scanId}`)); process.exit(1); } const { confirm } = await prompts({ type: 'confirm', name: 'confirm', message: `Delete scan of ${scan.target}?`, initial: false }); if (confirm) { const deleted = deleteScan(scanId); if (deleted) { console.log(chalk.green('โœ… Scan deleted')); } else { console.error(chalk.red('โŒ Failed to delete scan')); } } else { console.log(chalk.yellow('Delete cancelled')); } } catch (error) { console.error(chalk.red(`โŒ Delete error: ${error.message}`)); process.exit(1); } }); // Project management program .command('projects') .description('Manage scanning projects') .option('--add', 'Add a new project') .option('--remove <project>', 'Remove a project by ID or name') .option('--list', 'List all projects') .option('--show <project>', 'Show project details') .option('--history <project>', 'Show project scan history') .option('--stats [project]', 'Show project statistics (or global if no project specified)') .option('--clear-history [project]', 'Clear project history (or all history if no project specified)') .option('--name <name>', 'Project name (for --add)') .option('--domain <domain>', 'Project domain (for --add)') .option('--url <url>', 'Project URL (for --add)') .option('--description <description>', 'Project description (for --add)') .action(async (options) => { try { if (options.add) { if (!options.name) { console.error(chalk.red('โŒ Project name is required (use --name)')); process.exit(1); } if (!options.domain && !options.url) { console.error(chalk.red('โŒ Either --domain or --url is required')); process.exit(1); } const projectData = { name: options.name, domain: options.domain, url: options.url, description: options.description }; const project = addProject(projectData); console.log(chalk.green(`โœ… Project created: ${project.name}`)); console.log(chalk.gray(` ID: ${project.id}`)); console.log(chalk.gray(` Domain: ${project.domain || 'N/A'}`)); console.log(chalk.gray(` URL: ${project.url || 'N/A'}`)); if (project.description) { console.log(chalk.gray(` Description: ${project.description}`)); } return; } if (options.remove) { const project = getProject(options.remove); if (!project) { console.error(chalk.red(`โŒ Project not found: ${options.remove}`)); process.exit(1); } const { confirm } = await prompts({ type: 'confirm', name: 'confirm', message: `Remove project "${project.name}" and all its scan history?`, initial: false }); if (confirm) { const removed = removeProject(options.remove); if (removed) { console.log(chalk.green(`โœ… Project removed: ${project.name}`)); } else { console.error(chalk.red('โŒ Failed to remove project')); } } else { console.log(chalk.yellow('Remove cancelled')); } return; } if (options.list) { const projects = getProjects(); if (projects.length === 0) { console.log(chalk.yellow('No projects found.')); console.log(chalk.cyan('\n๐Ÿ’ก Add a project with: scanner projects --add --name "My Project" --domain "example.com"')); return; } console.log(chalk.blue('๐Ÿ“ Projects\n')); projects.forEach((project, index) => { console.log(`${chalk.bold(project.name)}`); console.log(chalk.gray(` ID: ${project.id}`)); console.log(chalk.gray(` Domain: ${project.domain || 'N/A'}`)); console.log(chalk.gray(` URL: ${project.url || 'N/A'}`)); console.log(chalk.gray(` Scans: ${project.scanCount}`)); console.log(chalk.gray(` Created: ${new Date(project.createdAt).toLocaleString()}`)); if (project.lastScanAt) { console.log(chalk.gray(` Last Scan: ${new Date(project.lastScanAt).toLocaleString()}`)); } if (project.description) { console.log(chalk.gray(` Description: ${project.description}`)); } if (index < projects.length - 1) { console.log(); } }); return; } if (options.show) { const project = getProject(options.show); if (!project) { console.error(chalk.red(`โŒ Project not found: ${options.show}`)); process.exit(1); } console.log(chalk.blue('๐Ÿ“ Project Details\n')); console.log(chalk.bold(`Name: ${project.name}`)); console.log(`ID: ${chalk.gray(project.id)}`); console.log(`Domain: ${chalk.cyan(project.domain || 'N/A')}`); console.log(`URL: ${chalk.cyan(project.url || 'N/A')}`); console.log(`Scan Count: ${chalk.yellow(project.scanCount)}`); console.log(`Created: ${chalk.gray(new Date(project.createdAt).toLocaleString())}`); console.log(`Updated: ${chalk.gray(new Date(project.updatedAt).toLocaleString())}`); if (project.lastScanAt) { console.log(`Last Scan: ${chalk.gray(new Date(project.lastScanAt).toLocaleString())}`); } if (project.description) { console.log(`Description: ${chalk.gray(project.description)}`); } // Show recent scans const recentScans = getProjectHistory(project.id, 5); if (recentScans.length > 0) { console.log(chalk.bold('\n๐Ÿ“‹ Recent Scans:')); recentScans.forEach(scan => { const status = scan.status === 'completed' ? chalk.green('โœ…') : scan.status === 'failed' ? chalk.red('โŒ') : chalk.yellow('โณ'); console.log(`${status} ${chalk.gray(new Date(scan.startTime).toLocaleString())} - ${scan.summary?.total || 0} vulnerabilities`); }); } return; } if (options.history) { const project = getProject(options.history); if (!project) { console.error(chalk.red(`โŒ Project not found: ${options.history}`)); process.exit(1); } const history = getProjectHistory(project.id); if (history.length === 0) { console.log(chalk.yellow(`No scan history found for project: ${project.name}`)); return; } console.log(chalk.blue(`๐Ÿ“‹ Scan History for ${project.name}\n`)); history.forEach((scan, index) => { const status = scan.status === 'completed' ? chalk.green('โœ…') : scan.status === 'failed' ? chalk.red('โŒ') : chalk.yellow('โณ'); console.log(`${status} ${chalk.bold(scan.target)}`); console.log(chalk.gray(` Scan ID: ${scan.scanId}`)); console.log(chalk.gray(` Date: ${new Date(scan.startTime).toLocaleString()}`)); console.log(chalk.gray(` Duration: ${formatDuration(scan.duration || 0)}`)); console.log(chalk.gray(` Tools: ${scan.tools.join(', ')}`)); console.log(chalk.gray(` Vulnerabilities: ${scan.summary?.total || 0}`)); if (index < history.length - 1) { console.log(); } }); return; } if (options.stats !== undefined) { const projectId = options.stats || null; let project = null; if (projectId) { project = getProject(projectId); if (!project) { console.error(chalk.red(`โŒ Project not found: ${projectId}`)); process.exit(1); } } const stats = getProjectStats(projectId); console.log(chalk.blue(project ? `๐Ÿ“Š Statistics for ${project.name}` : '๐Ÿ“Š Global Statistics')); console.log(); console.log(chalk.bold('Overview:')); console.log(`Total Scans: ${chalk.cyan(stats.totalScans)}`); console.log(`Completed: ${chalk.green(stats.completedScans)}`); console.log(`Failed: ${chalk.red(stats.failedScans)}`); console.log(`Average Duration: ${chalk.yellow(formatDuration(stats.averageScanTime))}`); console.log(chalk.bold('\nVulnerabilities:')); console.log(`Total Found: ${chalk.cyan(stats.totalVulnerabilities)}`); displayVulnerabilitySummary(stats.severityBreakdown); if (Object.keys(stats.toolUsage).length > 0) { console.log(chalk.bold('\nTool Usage:')); Object.entries(stats.toolUsage) .sort(([,a], [,b]) => b - a) .forEach(([tool, count]) => { console.log(`${chalk.cyan(tool)}: ${count} times`); }); } if (Object.keys(stats.scansByMonth).length > 0) { console.log(chalk.bold('\nScans by Month:')); Object.entries(stats.scansByMonth) .sort(([a], [b]) => b.localeCompare(a)) .slice(0, 6) .forEach(([month, count]) => { console.log(`${chalk.cyan(month)}: ${count} scans`); }); } return; } if (options.clearHistory !== undefined) { const projectId = options.clearHistory || null; let project = null; if (projectId) { project = getProject(projectId); if (!project) { console.error(chalk.red(`โŒ Project not found: ${projectId}`)); process.exit(1); } } const { confirm } = await prompts({ type: 'confirm', name: 'confirm', message: project ? `Clear scan history for project "${project.name}"?` : 'Clear ALL scan history for ALL projects?', initial: false }); if (confirm) { clearProjectHistory(projectId); console.log(chalk.green(project ? `โœ… Scan history cleared for project: ${project.name}` : 'โœ… All scan history cleared' )); } else { console.log(chalk.yellow('Clear cancelled')); } return; } // Default: show help console.log(chalk.blue('๐Ÿ“ Project Management\n')); console.log('Available commands:'); console.log(' --add Add a new project (requires --name and --domain or --url)'); console.log(' --remove <project> Remove a project by ID or name'); console.log(' --list List all projects'); console.log(' --show <project> Show project details'); console.log(' --history <project> Show project scan history'); console.log(' --stats [project] Show statistics (project or global)'); console.log(' --clear-history [project] Clear scan history'); console.log('\nProject creation options:'); console.log(' --name <name> Project name (required)'); console.log(' --domain <domain> Project domain'); console.log(' --url <url> Project URL'); console.log(' --description <desc> Project description'); console.log('\nExamples:'); console.log(' scanner projects --add --name "My Website" --domain "example.com"'); console.log(' scanner projects --add --name "API Server" --url "https://api.example.com"'); console.log(' scanner projects --list'); console.log(' scanner projects --show "My Website"'); console.log(' scanner projects --history "My Website"'); } catch (error) { console.error(chalk.red(`โŒ Project management error: ${error.message}`)); process.exit(1); } }); // Set program info program .name('scanner') .description('Web application security scanner with OWASP compliance') .version('1.0.0'); // Parse command line arguments program.parse(process.argv); // Show help if no command provided if (!process.argv.slice(2).length) { program.outputHelp(); }