UNPKG

pury

Version:

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

264 lines 12.6 kB
import { FindingType } from './types/index.js'; import { ConfigManager } from './config/config-manager.js'; import { FileScanner } from './scanner/file-scanner.js'; import { MalwareAnalyzer } from './analyzers/malware-analyzer.js'; import { SecretsAnalyzer } from './analyzers/secrets-analyzer.js'; import { DependencyAnalyzer } from './analyzers/dependency-analyzer.js'; import { QualityAnalyzer } from './analyzers/quality-analyzer.js'; import { GeminiClient } from './ai/gemini-client.js'; import { ContextManager } from './ai/context-manager.js'; import { logger } from './utils/logger.js'; import { validateScanOptions } from './utils/validation.js'; export class PuryAI { config; configManager; fileScanner; geminiClient; contextManager; constructor() { this.configManager = new ConfigManager(); this.config = {}; // Will be loaded in loadConfig this.fileScanner = new FileScanner(); this.contextManager = new ContextManager(); } async loadConfig(configPath) { try { this.config = await this.configManager.loadConfig(configPath); logger.debug('Configuration loaded successfully'); // Initialize file scanner with config this.fileScanner = new FileScanner(this.config.scanner.include, this.config.scanner.exclude, this.config.scanner.maxFileSize, this.config.scanner.followSymlinks); // Initialize AI client if API key is available const apiKey = process.env.GEMINI_API_KEY || this.config.ai.gemini?.apiKey; if (apiKey && this.config.ai.provider === 'gemini') { this.geminiClient = new GeminiClient({ apiKey, model: this.config.ai.gemini?.model || 'gemini-2.5-flash', temperature: this.config.ai.gemini?.temperature || 0.1, maxTokens: this.config.ai.gemini?.maxTokens || 2048 }); logger.debug('Gemini AI client initialized'); } } catch (error) { logger.warn(`Failed to load configuration: ${error.message}`); // Use default configuration this.config = (await import('./config/defaults.js')).DEFAULT_CONFIG; } } async scan(scanOptions, analyzerConfig = {}, onProgress) { const startTime = Date.now(); // Validate input await validateScanOptions(scanOptions); logger.info(`Starting scan of: ${scanOptions.path}`); // Step 1: Scan files const files = await this.fileScanner.scan(scanOptions); logger.info(`Found ${files.length} files to analyze`); onProgress?.('files_found', 0, files.length); if (files.length === 0) { return this.createEmptyReport(startTime); } // Step 2: Run analyzers const analyzerResults = []; const enabledAnalyzers = analyzerConfig.analyzers || ['malware', 'secrets']; const sensitivity = analyzerConfig.sensitivity || 'medium'; let completedAnalyzers = 0; const totalAnalyzers = enabledAnalyzers.length + (analyzerConfig.useAI !== false && this.geminiClient ? 1 : 0); // Run static analyzers if (enabledAnalyzers.includes('malware')) { logger.debug('Running malware analysis...'); onProgress?.('analyzer_start', completedAnalyzers, totalAnalyzers, '🦠 Malware Detection'); const malwareAnalyzer = new MalwareAnalyzer(sensitivity); // File-by-file progress for malware analysis const malwareProgress = (current, total, currentFile) => { const fileName = currentFile.split('/').pop() || currentFile; onProgress?.('file_progress', current, total, `🦠 Malware scanning: ${fileName}`); }; const result = await malwareAnalyzer.analyze(files, malwareProgress); analyzerResults.push(result); completedAnalyzers++; onProgress?.('analyzer_complete', completedAnalyzers, totalAnalyzers, `🦠 Malware Detection - ${result.findings.length} issues found`); logger.debug(`Malware analysis found ${result.findings.length} issues`); } if (enabledAnalyzers.includes('secrets')) { logger.debug('Running secrets analysis...'); onProgress?.('analyzer_start', completedAnalyzers, totalAnalyzers, '🔐 Secret Scanning'); const secretsAnalyzer = new SecretsAnalyzer(sensitivity); // File-by-file progress for secrets analysis const secretsProgress = (current, total, currentFile) => { const fileName = currentFile.split('/').pop() || currentFile; onProgress?.('file_progress', current, total, `🔐 Secret scanning: ${fileName}`); }; const result = await secretsAnalyzer.analyze(files, secretsProgress); analyzerResults.push(result); completedAnalyzers++; onProgress?.('analyzer_complete', completedAnalyzers, totalAnalyzers, `🔐 Secret Scanning - ${result.findings.length} secrets found`); logger.debug(`Secrets analysis found ${result.findings.length} issues`); } if (enabledAnalyzers.includes('vulnerabilities')) { logger.debug('Running dependency analysis...'); onProgress?.('analyzer_start', completedAnalyzers, totalAnalyzers, '🛡️ Vulnerability Analysis'); const dependencyAnalyzer = new DependencyAnalyzer(); // File-by-file progress for dependency analysis const dependencyProgress = (current, total, currentFile) => { const fileName = currentFile.split('/').pop() || currentFile; onProgress?.('file_progress', current, total, `🛡️ Dependency scanning: ${fileName}`); }; const result = await dependencyAnalyzer.analyze(files, dependencyProgress); analyzerResults.push(result); completedAnalyzers++; onProgress?.('analyzer_complete', completedAnalyzers, totalAnalyzers, `🛡️ Vulnerability Analysis - ${result.findings.length} vulnerabilities found`); logger.debug(`Dependency analysis found ${result.findings.length} issues`); } if (enabledAnalyzers.includes('quality')) { logger.debug('Running code quality analysis...'); onProgress?.('analyzer_start', completedAnalyzers, totalAnalyzers, '⚡ Code Quality Check'); const qualityAnalyzer = new QualityAnalyzer(sensitivity); // File-by-file progress for quality analysis const qualityProgress = (current, total, currentFile) => { const fileName = currentFile.split('/').pop() || currentFile; onProgress?.('file_progress', current, total, `⚡ Quality checking: ${fileName}`); }; const result = await qualityAnalyzer.analyze(files, qualityProgress); analyzerResults.push(result); completedAnalyzers++; onProgress?.('analyzer_complete', completedAnalyzers, totalAnalyzers, `⚡ Code Quality Check - ${result.findings.length} issues found`); logger.debug(`Quality analysis found ${result.findings.length} issues`); } // Step 3: Run AI analysis if enabled and available if (analyzerConfig.useAI !== false && this.geminiClient) { try { logger.debug('Running AI-powered analysis...'); onProgress?.('analyzer_start', completedAnalyzers, totalAnalyzers, '🤖 AI-Powered Analysis'); const aiResult = await this.runAIAnalysis(files, enabledAnalyzers, onProgress); if (aiResult) { analyzerResults.push(aiResult); completedAnalyzers++; onProgress?.('analyzer_complete', completedAnalyzers, totalAnalyzers, `🤖 AI Analysis - ${aiResult.findings.length} AI insights found`); logger.debug(`AI analysis found ${aiResult.findings.length} additional issues`); } } catch (error) { logger.warn(`AI analysis failed: ${error.message}`); completedAnalyzers++; onProgress?.('analyzer_complete', completedAnalyzers, totalAnalyzers, '🤖 AI Analysis - Failed'); } } // Step 4: Combine results and create report const allFindings = analyzerResults.flatMap(result => result.findings); const scanDuration = Date.now() - startTime; const report = this.createReport(files, allFindings, scanDuration, scanOptions); logger.success(`Scan completed in ${(scanDuration / 1000).toFixed(2)}s - found ${allFindings.length} issues`); return report; } async runAIAnalysis(files, enabledAnalyzers, onProgress) { if (!this.geminiClient) { return null; } const startTime = Date.now(); // Create analysis requests const analysisTypes = enabledAnalyzers.map(analyzer => { switch (analyzer) { case 'malware': return FindingType.MALWARE; case 'secrets': return FindingType.SECRET; case 'vulnerabilities': return FindingType.VULNERABILITY; case 'quality': return FindingType.CODE_QUALITY; default: return FindingType.CODE_QUALITY; } }); // Process requests in batches const batchedRequests = this.contextManager.createBatchedRequests(files, analysisTypes); const allResponses = []; let processedBatches = 0; for (const batch of batchedRequests) { onProgress?.('ai_analysis', processedBatches, batchedRequests.length); // Show individual file being processed for (const request of batch) { onProgress?.('file_scan', 0, 1, request.filePath); } const responses = await this.geminiClient.analyzeMultipleFiles(batch); allResponses.push(...responses); processedBatches++; } onProgress?.('ai_analysis', processedBatches, batchedRequests.length); // Combine findings from all responses const allFindings = allResponses.flatMap(response => response.findings); return { findings: allFindings, processingTime: Date.now() - startTime, filesAnalyzed: files.length }; } createReport(files, findings, scanDuration, scanOptions) { // Calculate severity counts const severityCount = { low: 0, medium: 0, high: 0, critical: 0 }; for (const finding of findings) { severityCount[finding.severity]++; } return { summary: { filesScanned: files.length, threatsFound: findings.length, severityCount, scanDuration }, findings, metadata: { version: '0.1.0', timestamp: new Date().toISOString(), scanOptions, ...(this.geminiClient && { aiProvider: 'gemini' }) } }; } createEmptyReport(startTime) { return { summary: { filesScanned: 0, threatsFound: 0, severityCount: { low: 0, medium: 0, high: 0, critical: 0 }, scanDuration: Date.now() - startTime }, findings: [], metadata: { version: '0.1.0', timestamp: new Date().toISOString(), scanOptions: { path: '' } } }; } async testAIConnection() { if (!this.geminiClient) { return false; } try { return await this.geminiClient.testConnection(); } catch { return false; } } getConfig() { return this.config; } async createConfig(path) { await this.configManager.initConfig(path); } } // Export everything for external use export * from './types/index.js'; export * from './analyzers/index.js'; export * from './scanner/index.js'; export * from './config/index.js'; export * from './utils/index.js'; export * from './reporters/index.js'; //# sourceMappingURL=index.js.map