UNPKG

@moikas/code-audit-mcp

Version:

AI-powered code auditing via MCP using local Ollama models for security, performance, and quality analysis

490 lines 16.7 kB
/** * Base auditor abstract class with common functionality */ import { randomUUID } from 'crypto'; import { generatePrompt, generateFastModePrompt } from '../ollama/prompts.js'; export class BaseAuditor { auditType; config; ollamaClient; modelManager; constructor(config, ollamaClient, modelManager) { this.config = config; this.ollamaClient = ollamaClient; this.modelManager = modelManager; } /** * Main audit method - implements the core audit workflow */ async audit(request) { const startTime = Date.now(); const requestId = randomUUID(); try { // Validate request this.validateRequest(request); // Check if this auditor is enabled for the request if (!this.shouldAudit(request)) { return this.createEmptyResult(requestId, startTime); } // Detect language and prepare context const language = this.detectLanguage(request.code, request.language); const codeMetrics = await this.analyzeCodeMetrics(request.code, language); // Select appropriate model const model = this.selectModel(request, language); if (!model) { throw this.createError('NO_AVAILABLE_MODEL', 'No suitable model available for audit'); } // Generate audit prompt const promptContext = { code: request.code, language, auditType: this.auditType, context: request.context, codeMetrics, }; const prompt = request.priority === 'fast' ? generateFastModePrompt(promptContext) : generatePrompt(promptContext); // Perform AI analysis const aiStartTime = Date.now(); const aiResponse = await this.ollamaClient.generate({ model, prompt, temperature: this.getTemperature(), max_tokens: this.getMaxTokens(), }); const aiEndTime = Date.now(); // Parse and validate AI response const rawIssues = await this.parseAIResponse(aiResponse.response); // Post-process issues const issues = await this.postProcessIssues(rawIssues, request, language); // Filter and sort issues const filteredIssues = this.filterIssues(issues, request); const sortedIssues = this.sortIssues(filteredIssues); // Create final result const result = this.createResult(requestId, sortedIssues, model, startTime, aiEndTime - aiStartTime, codeMetrics); return result; } catch (error) { console.error(`Audit failed for ${this.auditType}:`, error); throw error instanceof Error ? error : this.createError('AUDIT_FAILED', 'Unknown audit error'); } } /** * Validate the audit request */ validateRequest(request) { if (!request.code?.trim()) { throw this.createError('INVALID_REQUEST', 'Code is required and cannot be empty'); } if (!request.language?.trim()) { throw this.createError('INVALID_REQUEST', 'Language is required'); } if (request.code.length > 50000) { throw this.createError('CODE_TOO_LARGE', 'Code exceeds maximum size limit (50KB)'); } } /** * Check if this auditor should run for the given request */ shouldAudit(request) { // Check if auditor is enabled if (!this.config.enabled) { return false; } // Check if audit type matches (or if it's 'all') if (request.auditType !== 'all' && request.auditType !== this.auditType) { return false; } // Check fast mode compatibility if (request.priority === 'fast') { return this.auditType === 'security' || this.auditType === 'completeness'; } return true; } /** * Detect or validate the programming language */ detectLanguage(code, declaredLanguage) { // For now, trust the declared language // Could implement language detection heuristics here return declaredLanguage.toLowerCase(); } /** * Analyze basic code metrics */ async analyzeCodeMetrics(code, _language) { const lines = code.split('\n'); const lineCount = lines.length; // Simple function counting (language-agnostic) const functionPatterns = [ /function\s+\w+/g, // JavaScript/TypeScript functions /def\s+\w+/g, // Python functions /\w+\s*\([^)]*\)\s*{/g, // C-style functions /fn\s+\w+/g, // Rust functions /func\s+\w+/g, // Go functions ]; let functionCount = 0; for (const pattern of functionPatterns) { const matches = code.match(pattern); if (matches) { functionCount += matches.length; } } // Simple complexity estimation const complexityPatterns = [ /\bif\b/g, /\belse\b/g, /\bwhile\b/g, /\bfor\b/g, /\bswitch\b/g, /\btry\b/g, /\bcatch\b/g, ]; let complexity = 1; // Base complexity for (const pattern of complexityPatterns) { const matches = code.match(pattern); if (matches) { complexity += matches.length; } } return { lineCount, functionCount, complexity: Math.round(complexity / Math.max(functionCount, 1)), }; } /** * Select the best model for this audit */ selectModel(request, language) { const availableModels = this.ollamaClient.getAvailableModels(); const priority = request.priority || 'thorough'; return this.modelManager.selectModel(this.auditType, language, priority, availableModels); } /** * Get temperature for model generation */ getTemperature() { return 0.1; // Low temperature for consistent, factual analysis } /** * Get max tokens for model generation */ getMaxTokens() { return 2048; // Sufficient for structured issue reports } /** * Parse AI response into structured issues */ async parseAIResponse(response) { try { // Extract JSON from response (handle markdown code blocks) const jsonMatch = response.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || response.match(/(\{[\s\S]*\})/); if (!jsonMatch) { throw new Error('No JSON found in AI response'); } const jsonStr = jsonMatch[1]; const parsed = JSON.parse(jsonStr); if (!parsed.issues || !Array.isArray(parsed.issues)) { throw new Error('Invalid response format: missing issues array'); } return parsed.issues; } catch (error) { console.error('Failed to parse AI response:', error); console.error('Raw response:', response); // Try to extract issues manually if JSON parsing fails return this.fallbackParseResponse(response); } } /** * Fallback parsing when JSON parsing fails */ fallbackParseResponse(response) { // Very basic fallback - look for line numbers and descriptions const issues = []; const lines = response.split('\n'); for (const line of lines) { const lineMatch = line.match(/line\s*(\d+)/i); if (lineMatch) { issues.push({ location: { line: parseInt(lineMatch[1]) }, severity: 'medium', type: 'parse_fallback', category: this.auditType, title: 'Issue detected (parsing fallback)', description: line.trim(), confidence: 0.3, fixable: false, }); } } return issues; } /** * Post-process and validate issues from AI */ async postProcessIssues(rawIssues, request, _language) { const issues = []; for (const raw of rawIssues) { try { const issue = this.validateAndNormalizeIssue(raw, request, _language); if (issue) { issues.push(issue); } } catch (error) { console.warn('Failed to process issue:', error, raw); // Continue processing other issues } } return issues; } /** * Validate and normalize a single issue */ validateAndNormalizeIssue(raw, request, _language) { // Required fields validation if (!raw.location?.line || !raw.title || !raw.description) { return null; } // Normalize severity const severity = this.normalizeSeverity(raw.severity); // Generate unique ID const id = randomUUID(); // Ensure confidence is in valid range const confidence = Math.max(0, Math.min(1, raw.confidence || 0.5)); // Validate line number const codeLines = request.code.split('\n'); const line = Math.max(1, Math.min(codeLines.length, raw.location.line)); return { id, location: { line, column: raw.location.column, endLine: raw.location.endLine, endColumn: raw.location.endColumn, }, severity, type: raw.type || 'unknown', category: this.auditType, title: raw.title.substring(0, 200), // Limit title length description: raw.description.substring(0, 1000), // Limit description length suggestion: raw.suggestion?.substring(0, 1000), codeSnippet: this.extractCodeSnippet(request.code, line), confidence, fixable: Boolean(raw.fixable), ruleId: raw.ruleId, documentation: raw.documentation, impact: raw.impact, effort: raw.effort || 'medium', }; } /** * Normalize severity to valid values */ normalizeSeverity(severity) { const validSeverities = [ 'critical', 'high', 'medium', 'low', 'info', ]; const normalized = severity?.toLowerCase(); return validSeverities.includes(normalized) ? normalized : 'medium'; } /** * Extract code snippet around a specific line */ extractCodeSnippet(code, line, context = 2) { const lines = code.split('\n'); const start = Math.max(0, line - context - 1); const end = Math.min(lines.length, line + context); return lines.slice(start, end).join('\n'); } /** * Filter issues based on configuration and request */ filterIssues(issues, request) { let filtered = issues; // Filter by severity if (this.config.severity.length > 0) { filtered = filtered.filter((issue) => this.config.severity.includes(issue.severity)); } // Filter by enabled rules filtered = filtered.filter((issue) => { const ruleId = issue.ruleId || issue.type; return this.config.rules[ruleId] !== false; }); // Limit number of issues if requested if (request.maxIssues && request.maxIssues > 0) { filtered = filtered.slice(0, request.maxIssues); } return filtered; } /** * Sort issues by severity and line number */ sortIssues(issues) { const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4, }; return issues.sort((a, b) => { // First by severity const severityDiff = severityOrder[a.severity] - severityOrder[b.severity]; if (severityDiff !== 0) { return severityDiff; } // Then by line number return a.location.line - b.location.line; }); } /** * Create audit result */ createResult(requestId, issues, model, startTime, aiResponseTime, codeMetrics) { const endTime = Date.now(); const totalDuration = endTime - startTime; return { requestId, issues, summary: this.createSummary(issues), coverage: this.createCoverage(codeMetrics, issues), suggestions: this.createSuggestions(issues), metrics: { duration: totalDuration, modelResponseTime: aiResponseTime, parsingTime: totalDuration - aiResponseTime, postProcessingTime: Math.max(0, totalDuration - aiResponseTime - 100), }, model, timestamp: new Date().toISOString(), version: '1.0.0', }; } /** * Create audit summary */ createSummary(issues) { const summary = { total: issues.length, critical: 0, high: 0, medium: 0, low: 0, info: 0, byCategory: {}, byType: {}, }; for (const issue of issues) { summary[issue.severity]++; summary.byCategory[issue.category] = (summary.byCategory[issue.category] || 0) + 1; summary.byType[issue.type] = (summary.byType[issue.type] || 0) + 1; } return summary; } /** * Create coverage information */ createCoverage(codeMetrics, _issues) { return { linesAnalyzed: codeMetrics.lineCount, functionsAnalyzed: codeMetrics.functionCount, classesAnalyzed: 0, // Could be implemented with better parsing complexity: codeMetrics.complexity, }; } /** * Create suggestions based on issues */ createSuggestions(issues) { return { autoFixable: issues.filter((issue) => issue.fixable), priorityFixes: issues.filter((issue) => issue.severity === 'critical' || issue.severity === 'high'), quickWins: issues.filter((issue) => issue.effort === 'low' && (issue.severity === 'medium' || issue.severity === 'high')), technicalDebt: issues.filter((issue) => issue.category === 'quality' || issue.category === 'architecture'), }; } /** * Create empty result for disabled auditors */ createEmptyResult(requestId, startTime) { return { requestId, issues: [], summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0, info: 0, byCategory: {}, byType: {}, }, coverage: { linesAnalyzed: 0, functionsAnalyzed: 0, classesAnalyzed: 0, complexity: 0, }, suggestions: { autoFixable: [], priorityFixes: [], quickWins: [], technicalDebt: [], }, metrics: { duration: Date.now() - startTime, modelResponseTime: 0, parsingTime: 0, postProcessingTime: 0, }, model: 'none', timestamp: new Date().toISOString(), version: '1.0.0', }; } /** * Create standardized error */ createError(code, message, details) { return { code, message, details, recoverable: true, timestamp: new Date().toISOString(), }; } /** * Get audit type */ getAuditType() { return this.auditType; } /** * Update configuration */ updateConfig(config) { this.config = { ...this.config, ...config }; } /** * Get current configuration */ getConfig() { return { ...this.config }; } } //# sourceMappingURL=base.js.map