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