pury
Version:
🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis
255 lines • 11.4 kB
JavaScript
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