@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
JavaScript
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();
}