UNPKG

pury

Version:

🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis

255 lines 11.4 kB
import { Command } from 'commander'; import { resolve } from 'path'; import ora from 'ora'; import { PuryAI } from '../../index.js'; import { logger } from '../../utils/logger.js'; import { ValidationError } from '../../utils/validation.js'; export function createScanCommand() { return new Command('scan') .description('Scan files or directories for security threats and code quality issues') .argument('[path]', 'Path to scan (defaults to current directory)', '.') .option('-c, --config <file>', 'Configuration file path') .option('--exclude <patterns...>', 'Patterns to exclude from scanning') .option('--include <patterns...>', 'Patterns to include in scanning') .option('--max-file-size <size>', 'Maximum file size to scan in bytes', '1048576') .option('--format <type>', 'Output format (console, json, html, sarif)', 'console') .option('-o, --output <file>', 'Output file path') .option('--no-ai', 'Disable AI-powered analysis') .option('--analyzers <types...>', 'Specific analyzers to run (malware, secrets, vulnerabilities, quality)') .option('--sensitivity <level>', 'Analysis sensitivity (low, medium, high)', 'medium') .action(async (path, options) => { try { await runScan(path, options); } catch (error) { if (error instanceof ValidationError) { logger.error(`Validation error: ${error.message}`); if (error.field) { logger.error(`Field: ${error.field}`); } } else { logger.error(`Scan failed: ${error.message}`); } process.exit(1); } }); } async function runScan(scanPath, options) { const resolvedPath = resolve(scanPath); logger.info(`Starting security scan of: ${resolvedPath}`); // Initialize PuryAI const puryai = new PuryAI(); // Load configuration if (options.config) { await puryai.loadConfig(options.config); } else { await puryai.loadConfig(); } // Override config with command line options const scanOptions = { path: resolvedPath, exclude: options.exclude, include: options.include, maxFileSize: parseInt(options.maxFileSize, 10), recursive: true, followSymlinks: false }; // Configure analyzers const analyzerConfig = { useAI: !options.noAi, analyzers: options.analyzers || ['malware', 'secrets', 'vulnerabilities'], sensitivity: options.sensitivity }; // Create progress tracking with ora const spinner = ora('🚀 Initializing scan...').start(); // Progress callback with animations and emojis const onProgress = (step, current, total, currentFile) => { if (step === 'files_found') { spinner.text = `🔍 Discovered ${total} files to analyze`; } else if (step === 'analyzer_start') { const percentage = total > 0 ? Math.round((current / total) * 100) : 0; const progressBar = generateProgressBar(percentage); spinner.text = `${getSpinnerFrame()} ${currentFile || '🔬 Analyzing'} ${progressBar} ${percentage}% (${current}/${total})`; } else if (step === 'analyzer_complete') { const percentage = total > 0 ? Math.round((current / total) * 100) : 0; const progressBar = generateProgressBar(percentage); spinner.text = `✅ ${currentFile || 'Analysis complete'} ${progressBar} ${percentage}% (${current}/${total})`; } else if (step === 'analyzing') { const percentage = Math.round((current / total) * 100); const progressBar = generateProgressBar(percentage); spinner.text = `🔬 Processing... ${current}/${total} ${progressBar} ${percentage}%`; } else if (step === 'ai_analysis') { const percentage = total > 0 ? Math.round((current / total) * 100) : 0; const progressBar = generateProgressBar(percentage); const fileInfo = currentFile ? ` • ${currentFile.split('/').pop()}` : ''; spinner.text = `🤖 AI analyzing... ${current}/${total} ${progressBar} ${percentage}%${fileInfo}`; } else if (step === 'file_scan') { const fileName = currentFile ? currentFile.split('/').pop() : 'file'; spinner.text = `📄 Scanning ${fileName}...`; } else if (step === 'file_progress') { const percentage = total > 0 ? Math.round((current / total) * 100) : 0; const miniProgressBar = generateProgressBar(percentage, 8); spinner.text = `${getSpinnerFrame()} ${currentFile || '📄 Processing files'} ${miniProgressBar} ${percentage}% (${current}/${total})`; } }; // Helper function to generate enhanced progress bar const generateProgressBar = (percentage, length = 12) => { const filled = Math.round((percentage / 100) * length); // Different colors for different progress levels let bar = ''; for (let i = 0; i < length; i++) { if (i < filled) { if (percentage >= 80) { bar += '🟩'; // Green for high progress } else if (percentage >= 50) { bar += '🟨'; // Yellow for medium progress } else { bar += '🟦'; // Blue for low progress } } else { bar += '⬜'; // Empty } } return bar; }; // Animation frames for spinner const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; let frameIndex = 0; const getSpinnerFrame = () => { const frame = spinnerFrames[frameIndex]; frameIndex = (frameIndex + 1) % spinnerFrames.length; return frame; }; try { // Run the scan with progress tracking const report = await puryai.scan(scanOptions, analyzerConfig, onProgress); spinner.succeed(`✅ Scan completed! Found ${report.summary.threatsFound} potential issues`); // Output results await outputResults(report, options.format, options.output); // Exit with appropriate code const hasHighSeverityIssues = report.findings.some((f) => f.severity === 'high' || f.severity === 'critical'); if (hasHighSeverityIssues) { logger.warn('High severity issues found!'); process.exit(1); } } catch (error) { spinner.fail('❌ Scan failed'); throw error; } } async function addPuryToGitignore(currentDir) { const path = await import('path'); const fs = await import('fs/promises'); try { const gitignorePath = path.join(currentDir, '.gitignore'); const puryEntry = '.pury'; let gitignoreContent = ''; let gitignoreExists = true; // Check if .gitignore exists try { gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); } catch (error) { // .gitignore doesn't exist gitignoreExists = false; logger.info('📝 Creating .gitignore file'); } // Check if .pury is already in .gitignore const lines = gitignoreContent.split('\n'); const hasPuryEntry = lines.some(line => line.trim() === puryEntry || line.trim() === `${puryEntry}/` || line.trim() === `/${puryEntry}` || line.trim() === `/${puryEntry}/`); if (!hasPuryEntry) { // Add .pury entry with a descriptive comment const newContent = gitignoreExists && gitignoreContent.trim() ? `${gitignoreContent.trim()}\n\n# PuryAI scan results\n${puryEntry}\n` : `# PuryAI scan results\n${puryEntry}\n`; await fs.writeFile(gitignorePath, newContent, 'utf8'); logger.info('📝 Added .pury to .gitignore'); } } catch (error) { logger.warn(`Failed to update .gitignore: ${error.message}`); } } async function outputResults(report, format, outputFile) { const { ConsoleReporter, JsonReporter, HtmlReporter, SarifReporter, MarkdownReporter, SummaryReporter } = await import('../../reporters/index.js'); const path = await import('path'); const fs = await import('fs/promises'); let reporter; switch (format.toLowerCase()) { case 'json': reporter = new JsonReporter(); break; case 'html': reporter = new HtmlReporter(); break; case 'sarif': reporter = new SarifReporter(); break; case 'markdown': case 'md': reporter = new MarkdownReporter(); break; case 'console': default: reporter = new ConsoleReporter(); break; } if (outputFile) { await reporter.writeToFile(report, outputFile); logger.success(`Results saved to: ${outputFile}`); } else { await reporter.output(report); } // Always save both detailed and summary markdown results to .pury folder in current working directory try { const currentDir = process.cwd(); const puryDir = path.join(currentDir, '.pury'); // Create .pury directory if it doesn't exist await fs.mkdir(puryDir, { recursive: true }); // Auto-add .pury to .gitignore await addPuryToGitignore(currentDir); // Generate timestamp for unique filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; const timeStamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[1]?.split('.')[0] || 'unknown'; // Create both detailed and summary reports const detailedFileName = `scan-results-detailed-${timestamp}-${timeStamp}.md`; const summaryFileName = `scan-results-summary-${timestamp}-${timeStamp}.md`; const detailedPath = path.join(puryDir, detailedFileName); const summaryPath = path.join(puryDir, summaryFileName); // Also create/update latest results files const latestDetailedPath = path.join(puryDir, 'latest-scan-results.md'); const latestSummaryPath = path.join(puryDir, 'latest-scan-summary.md'); const markdownReporter = new MarkdownReporter(); const summaryReporter = new SummaryReporter(); // Generate detailed reports await markdownReporter.writeToFile(report, detailedPath); await markdownReporter.writeToFile(report, latestDetailedPath); // Generate summary reports await summaryReporter.writeToFile(report, summaryPath); await summaryReporter.writeToFile(report, latestSummaryPath); logger.info(`📝 Detailed results saved to: ${detailedPath}`); logger.info(`📋 Summary results saved to: ${summaryPath}`); logger.info(`📝 Latest detailed results: ${latestDetailedPath}`); logger.info(`📋 Latest summary results: ${latestSummaryPath}`); } catch (error) { logger.warn(`Failed to save markdown results: ${error.message}`); } } //# sourceMappingURL=scan.js.map