UNPKG

woaru

Version:

Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language

1,232 lines β€’ 75.9 kB
import { exec } from 'child_process'; import { promisify } from 'util'; import { ToolExecutor } from '../utils/toolExecutor.js'; import * as path from 'path'; import fs from 'fs-extra'; import { APP_CONFIG } from '../config/constants.js'; import i18next from 'i18next'; import { ToolsDatabaseManager, } from '../database/ToolsDatabaseManager.js'; import { EslintPlugin } from '../plugins/EslintPlugin.js'; import { SOLIDChecker } from '../solid/SOLIDChecker.js'; import { CodeSmellAnalyzer } from '../analyzer/CodeSmellAnalyzer.js'; import { hookManager, triggerHook, } from '../core/HookSystem.js'; const execAsync = promisify(exec); export class QualityRunner { notificationManager; databaseManager; corePlugins; solidChecker; codeSmellAnalyzer; constructor(notificationManager) { this.notificationManager = notificationManager; this.databaseManager = new ToolsDatabaseManager(); this.corePlugins = new Map(); this.solidChecker = new SOLIDChecker(); this.codeSmellAnalyzer = new CodeSmellAnalyzer(); // Initialize core plugins this.initializeCorePlugins(); } /** * Initialize secure core plugins */ initializeCorePlugins() { // Only add verified, secure core plugins this.corePlugins.set('EslintPlugin', new EslintPlugin()); // More core plugins would be added here as they're implemented // 🎣 Hook-System Debug aktivieren in Entwicklungsumgebung if (process.env.NODE_ENV === 'development') { hookManager.setDebugMode(true); } } /** * πŸ—‚οΈ Hilfsmethode: Sprache aus Dateierweiterung ableiten */ getLanguageFromExtension(ext) { const languageMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.go': 'go', '.rs': 'rust', '.java': 'java', '.cs': 'csharp', '.php': 'php', '.rb': 'ruby', '.cpp': 'cpp', '.c': 'c', '.h': 'c', '.hpp': 'cpp', }; return languageMap[ext.toLowerCase()] || 'unknown'; } /** * HYBRID ARCHITECTURE: Run quality checks using both core plugins and experimental tools * πŸͺ KI-freundliche Regelwelt: Integriert Hooks fΓΌr Erweiterbarkeit */ async runChecksOnFileChange(filePath) { const ext = path.extname(filePath).toLowerCase(); const relativePath = path.relative(process.cwd(), filePath); // πŸͺ HOOK: beforeAnalysis - KI-freundliche Regelwelt const beforeAnalysisData = { files: [relativePath], projectPath: process.cwd(), config: { fileExtension: ext, phase: 'file-change-analysis' }, timestamp: new Date(), }; try { await triggerHook('beforeAnalysis', beforeAnalysisData); } catch (hookError) { console.debug(`Hook error (beforeAnalysis): ${hookError}`); } try { // Phase 0: Always run internal code smell analysis first (no external dependencies) await this.runInternalCodeSmellAnalysis(relativePath, ext); // Phase 1: Try core plugins first (secure, established tools) const coreHandled = await this.runCorePluginCheck(relativePath, ext); if (coreHandled) { // πŸͺ HOOK: afterAnalysis - KI-freundliche Regelwelt (success with core plugin) const afterAnalysisData = { files: [relativePath], results: [ { file: relativePath, tool: 'core-plugin', success: true, issues: [], }, ], duration: 0, success: true, timestamp: new Date(), }; try { await triggerHook('afterAnalysis', afterAnalysisData); } catch (hookError) { console.debug(`Hook error (afterAnalysis core): ${hookError}`); } return; // Core plugin handled the file successfully } // Phase 2: Try experimental tools (dynamic command templates) const experimentalHandled = await this.runExperimentalToolCheck(relativePath, ext); if (experimentalHandled) { // πŸͺ HOOK: afterAnalysis - KI-freundliche Regelwelt (success with experimental tool) const afterAnalysisData = { files: [relativePath], results: [ { file: relativePath, tool: 'experimental-tool', success: true, issues: [], }, ], duration: 0, success: true, timestamp: new Date(), }; try { await triggerHook('afterAnalysis', afterAnalysisData); } catch (hookError) { console.debug(`Hook error (afterAnalysis experimental): ${hookError}`); } return; // Experimental tool handled the file successfully } // Phase 3: Fallback to legacy hardcoded checks await this.runLegacyChecks(relativePath, ext); // πŸͺ HOOK: afterAnalysis - KI-freundliche Regelwelt (success with legacy) const afterAnalysisData = { files: [relativePath], results: [ { file: relativePath, tool: 'legacy-check', success: true, issues: [], }, ], duration: 0, success: true, timestamp: new Date(), }; try { await triggerHook('afterAnalysis', afterAnalysisData); } catch (hookError) { console.debug(`Hook error (afterAnalysis legacy): ${hookError}`); } } catch (error) { // πŸͺ HOOK: onError - KI-freundliche Regelwelt const errorHookData = { error: error instanceof Error ? error : new Error(String(error)), context: 'file-change-analysis', filePath: relativePath, timestamp: new Date(), }; try { await triggerHook('onError', errorHookData); } catch (hookError) { console.debug(`Hook error (onError): ${hookError}`); } console.warn(i18next.t('quality_runner.check_failed', { file: relativePath }), error); } } /** * Phase 1: Run checks using secure core plugins * πŸͺ KI-freundliche Regelwelt: Integriert Tool-Execution Hooks */ async runCorePluginCheck(filePath, fileExtension) { try { // Get core tools that support this file extension const coreTools = await this.databaseManager.getCoreToolsForFileExtension(fileExtension); for (const coreTool of coreTools) { const plugin = this.corePlugins.get(coreTool.plugin_class); const pluginWithMethods = plugin; if (pluginWithMethods.canHandleFile && (await pluginWithMethods.canHandleFile(filePath))) { // πŸͺ HOOK: beforeToolExecution - KI-freundliche Regelwelt const beforeToolData = { toolName: coreTool.name, filePath, command: `${coreTool.name} ${filePath}`, timestamp: new Date(), }; try { await triggerHook('beforeToolExecution', beforeToolData); } catch (hookError) { console.debug(`Hook error (beforeToolExecution ${coreTool.name}): ${hookError}`); } console.log(`πŸ”§ ${i18next.t('quality_runner.core_plugin_running', { tool: coreTool.name, file: filePath })}`); if (pluginWithMethods.runLinter) { const result = await pluginWithMethods.runLinter(filePath, { fix: false, }); // πŸͺ HOOK: afterToolExecution - KI-freundliche Regelwelt const afterToolData = { toolName: coreTool.name, filePath, command: `${coreTool.name} ${filePath}`, output: result.output, exitCode: result.hasErrors ? 1 : 0, duration: 0, // Could track actual duration if needed success: !result.hasErrors, timestamp: new Date(), }; try { await triggerHook('afterToolExecution', afterToolData); } catch (hookError) { console.debug(`Hook error (afterToolExecution ${coreTool.name}): ${hookError}`); } if (result.hasErrors) { this.notificationManager.showCriticalQualityError(filePath, coreTool.name, result.output); } else if (result.hasWarnings) { console.log(`⚠️ ${i18next.t('quality_runner.tool_warnings', { tool: coreTool.name, file: filePath })}`); } else { this.notificationManager.showQualitySuccess(filePath, coreTool.name); } return true; // Successfully handled by core plugin } } } return false; // No core plugin could handle this file } catch (error) { console.warn(i18next.t('quality_runner.core_plugin_failed'), error); return false; } } /** * Phase 2: Run checks using experimental tools (dynamic command templates) * πŸͺ KI-freundliche Regelwelt: Integriert Tool-Execution Hooks */ async runExperimentalToolCheck(filePath, fileExtension) { try { // Get experimental tools that support this file extension const experimentalTools = await this.databaseManager.getExperimentalToolsForFileExtension(fileExtension); for (const experimentalTool of experimentalTools) { if (await this.canRunExperimentalTool(experimentalTool)) { // πŸͺ HOOK: beforeToolExecution - KI-freundliche Regelwelt const beforeToolData = { toolName: experimentalTool.name, filePath, command: experimentalTool.commandTemplate.replace('{filePath}', filePath), timestamp: new Date(), }; try { await triggerHook('beforeToolExecution', beforeToolData); } catch (hookError) { console.debug(`Hook error (beforeToolExecution ${experimentalTool.name}): ${hookError}`); } console.log(`πŸ§ͺ ${i18next.t('quality_runner.experimental_tool_running', { tool: experimentalTool.name, file: filePath })}`); const result = await this.executeExperimentalTool(experimentalTool, filePath); // πŸͺ HOOK: afterToolExecution - KI-freundliche Regelwelt const afterToolData = { toolName: experimentalTool.name, filePath, command: experimentalTool.commandTemplate.replace('{filePath}', filePath), output: result.output, exitCode: result.success ? 0 : 1, duration: 0, // Could track actual duration if needed success: result.success, timestamp: new Date(), }; try { await triggerHook('afterToolExecution', afterToolData); } catch (hookError) { console.debug(`Hook error (afterToolExecution ${experimentalTool.name}): ${hookError}`); } if (result.success) { if (result.output.includes('error')) { this.notificationManager.showCriticalQualityError(filePath, experimentalTool.name, result.output); } else { this.notificationManager.showQualitySuccess(filePath, experimentalTool.name); } return true; // Successfully handled by experimental tool } } } return false; // No experimental tool could handle this file } catch (error) { console.warn(i18next.t('quality_runner.experimental_tool_failed'), error); return false; } } /** * Phase 3: Fallback to legacy hardcoded checks */ async runLegacyChecks(filePath, fileExtension) { // Keep existing legacy logic for backward compatibility console.log(`πŸ“¦ ${i18next.t('quality_runner.legacy_check_running', { file: filePath })}`); // TypeScript/JavaScript files if (['.ts', '.tsx', '.js', '.jsx'].includes(fileExtension)) { await this.runESLintCheck(filePath); } // Python files if (fileExtension === '.py') { await this.runRuffCheck(filePath); } // Go files if (fileExtension === '.go') { await this.runGoCheck(filePath); } // Rust files if (fileExtension === '.rs') { await this.runRustCheck(filePath); } // C# files if (fileExtension === '.cs') { await this.runCSharpCheck(filePath); } // Java files if (fileExtension === '.java') { await this.runJavaCheck(filePath); } // PHP files if (fileExtension === '.php') { await this.runPHPCheck(filePath); } // Ruby files if (fileExtension === '.rb') { await this.runRubyCheck(filePath); } } // ========== EXPERIMENTAL TOOL EXECUTION ========== /** * Check if an experimental tool can be run (is installed) */ async canRunExperimentalTool(tool) { try { // Check if the tool is installed by trying to run it with --version const { stdout } = await execAsync(`${tool.commandTemplate.split(' ')[0]} --version`); return stdout.length > 0; } catch { return false; // Tool not installed or not in PATH } } /** * Execute an experimental tool using its command template */ async executeExperimentalTool(tool, filePath) { try { // Replace {filePath} in command template const command = tool.commandTemplate.replace('{filePath}', filePath); // Security: Validate command before execution if (!this.isValidExperimentalCommand(command, filePath)) { throw new Error(`Invalid experimental command: ${command}`); } const { stdout, stderr } = await execAsync(command, { timeout: 30000, // 30 second timeout cwd: path.dirname(filePath), }); return { success: true, output: stdout + stderr, }; } catch (error) { return { success: false, output: error instanceof Error ? error.message : 'Unknown error', }; } } /** * Validate experimental command for security */ isValidExperimentalCommand(command, filePath) { // Security checks for experimental commands // Must contain the file path if (!command.includes(filePath)) { return false; } // No dangerous shell operators const dangerousPatterns = [ '&&', '||', ';', '|', '>', '<', '`', '$', '(', ')', ]; for (const pattern of dangerousPatterns) { if (command.includes(pattern) && !command.includes(`'${pattern}'`) && !command.includes(`"${pattern}"`)) { return false; } } // Command must start with allowed prefixes const allowedPrefixes = ['npx', 'node', 'deno', 'bun']; const firstWord = command.split(' ')[0]; return allowedPrefixes.some(prefix => firstWord.startsWith(prefix)); } // New method for running checks on multiple files (for review command) async runChecksOnFileList(filePaths) { const results = []; for (const filePath of filePaths) { const ext = path.extname(filePath).toLowerCase(); const relativePath = path.relative(process.cwd(), filePath); try { // TypeScript/JavaScript files if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) { const language = ext.startsWith('.ts') ? 'typescript' : 'javascript'; // Always run internal code smell analysis first const codeSmellFindings = await this.codeSmellAnalyzer.analyzeFile(filePath, language); // Try ESLint check const eslintResult = await this.runESLintCheckForReview(relativePath); if (eslintResult) { // Add SOLID analysis and code smell findings to ESLint results const solidResult = await this.runSOLIDCheckForReview(filePath, language); eslintResult.solidResult = solidResult; eslintResult.codeSmellFindings = codeSmellFindings; results.push(eslintResult); } else if (codeSmellFindings.length > 0) { // No ESLint but we have code smell findings - create a result const issues = codeSmellFindings.map(finding => `Line ${finding.line}:${finding.column} - ${finding.message}`); const fixes = codeSmellFindings .filter(finding => finding.suggestion) .map(finding => finding.suggestion || 'No suggestion available'); const criticalFindings = codeSmellFindings.filter(f => f.severity === 'error'); const severity = criticalFindings.length > 0 ? 'error' : 'warning'; // Add SOLID analysis const solidResult = await this.runSOLIDCheckForReview(filePath, language); results.push({ filePath: relativePath, tool: 'WOARU Code Smell Analyzer', severity, issues, fixes: fixes.length > 0 ? fixes : undefined, explanation: `WOARU internal analysis found ${codeSmellFindings.length} code quality issues`, codeSmellFindings, solidResult, }); } } // Python files if (ext === '.py') { const result = await this.runRuffCheckForReview(relativePath); if (result) results.push(result); } // Go files if (ext === '.go') { const result = await this.runGoCheckForReview(relativePath); if (result) results.push(result); } // Rust files if (ext === '.rs') { const result = await this.runRustCheckForReview(relativePath); if (result) results.push(result); } // C# files if (ext === '.cs') { const result = await this.runCSharpCheckForReview(relativePath); if (result) results.push(result); } // Java files if (ext === '.java') { const result = await this.runJavaCheckForReview(relativePath); if (result) results.push(result); } // PHP files if (ext === '.php') { const result = await this.runPHPCheckForReview(relativePath); if (result) results.push(result); } // Ruby files if (ext === '.rb') { const result = await this.runRubyCheckForReview(relativePath); if (result) results.push(result); } } catch (error) { results.push({ filePath: relativePath, tool: 'Unknown', severity: 'error', issues: [`Failed to check file: ${error}`], }); } } // πŸͺ HOOK: onReportGeneration - KI-freundliche Regelwelt try { await triggerHook('onReportGeneration', { reportType: 'file-list-quality-check', data: results, timestamp: new Date(), }); } catch (hookError) { console.debug(`Hook error (onReportGeneration): ${hookError}`); } return results; } async runESLintCheck(filePath) { try { await ToolExecutor.runESLint(filePath, { cwd: path.dirname(filePath), }); this.notificationManager.showQualitySuccess(filePath, 'ESLint'); } catch (error) { // ESLint failed - extract error output const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); this.notificationManager.showCriticalQualityError(filePath, 'ESLint', String(output)); } } async runRuffCheck(filePath) { try { await ToolExecutor.runRuff(filePath, true); // true = fix enabled this.notificationManager.showQualitySuccess(filePath, 'Ruff'); } catch (error) { // Ruff failed - extract error output const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); this.notificationManager.showCriticalQualityError(filePath, 'Ruff', String(output)); } } async runGoCheck(filePath) { try { // First run gofmt to check formatting const { stdout: gofmtOutput } = await ToolExecutor.runGoFmt(filePath); // If gofmt returns output, the file needs formatting if (gofmtOutput.trim()) { // Get the diff to show what needs to be changed const { stdout: diffOutput } = await execAsync(`gofmt -d "${filePath}"`); this.notificationManager.showCriticalQualityError(filePath, 'gofmt', `File is not properly formatted. Run 'gofmt -w ${filePath}' to fix.\n\n${diffOutput}`); return; } // Also run go vet for additional checks await ToolExecutor.runGoVet(filePath); this.notificationManager.showQualitySuccess(filePath, 'Go (gofmt + go vet)'); } catch (error) { // go vet failed - extract error output const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); this.notificationManager.showCriticalQualityError(filePath, 'go vet', String(output)); } } async runRustCheck(filePath) { let hasErrors = false; let errorOutput = ''; try { // First check formatting with rustfmt await ToolExecutor.runRustFmt(filePath); // rustfmt returns non-zero exit code if formatting is needed // We'll catch this in the error handler below } catch (fmtError) { if (fmtError?.message ?.toString() ?.includes('Diff in')) { hasErrors = true; errorOutput += `Formatting issues found. Run 'rustfmt ${filePath}' to fix.\n\n`; errorOutput += fmtError?.stdout || fmtError?.stderr || fmtError?.message || 'Unknown error'; } } try { // Run clippy for linting await execAsync(`cargo clippy --manifest-path=$(dirname "${filePath}")/Cargo.toml -- -D warnings 2>&1`); if (!hasErrors) { this.notificationManager.showQualitySuccess(filePath, 'Rust (rustfmt + clippy)'); } } catch (clippyError) { hasErrors = true; const clippyOutput = clippyError?.stdout || clippyError?.stderr || clippyError?.message || 'Unknown error'; // Add separator if we already have formatting errors if (errorOutput) { errorOutput += '\n\n--- Clippy Warnings ---\n\n'; } errorOutput += clippyOutput; } if (hasErrors) { this.notificationManager.showCriticalQualityError(filePath, 'Rust (rustfmt + clippy)', errorOutput); } } async runCSharpCheck(filePath) { try { // Run dotnet format to check and fix formatting await execAsync(`dotnet format --verify-no-changes --include "${filePath}" 2>&1`); // If no output and success, file is properly formatted this.notificationManager.showQualitySuccess(filePath, 'dotnet format'); } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); // Check if it's a formatting issue if (output.includes('Formatted code file') || output.includes('needs formatting')) { this.notificationManager.showCriticalQualityError(filePath, 'dotnet format', `File needs formatting. Run 'dotnet format --include ${filePath}' to fix.\n\n${output}`); } else { this.notificationManager.showCriticalQualityError(filePath, 'dotnet format', output); } } } async runJavaCheck(filePath) { try { // Try to use google-java-format for formatting check await execAsync(`java -jar google-java-format.jar --dry-run --set-exit-if-changed "${filePath}" 2>&1`); this.notificationManager.showQualitySuccess(filePath, 'Java Format'); } catch { // If google-java-format is not available, try checkstyle try { await ToolExecutor.runCheckstyle(filePath); this.notificationManager.showQualitySuccess(filePath, 'Checkstyle'); } catch (checkstyleError) { const output = String(checkstyleError?.stdout || checkstyleError?.stderr || checkstyleError?.message || 'Unknown error'); this.notificationManager.showCriticalQualityError(filePath, 'Java Quality', output); } } } async runPHPCheck(filePath) { try { // Run PHP_CodeSniffer await ToolExecutor.runPhpCs(filePath, 'PSR12'); this.notificationManager.showQualitySuccess(filePath, 'PHP_CodeSniffer'); } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); // PHPCS returns non-zero exit code when issues are found if (output.includes('FOUND') && output.includes('ERROR')) { this.notificationManager.showCriticalQualityError(filePath, 'PHP_CodeSniffer', `${output}\n\nRun 'phpcbf --standard=PSR12 ${filePath}' to automatically fix some issues.`); } else { this.notificationManager.showCriticalQualityError(filePath, 'PHP_CodeSniffer', output); } } } async runRubyCheck(filePath) { try { // Run RuboCop with auto-correct in dry-run mode await ToolExecutor.runRuboCop(filePath, 'simple'); this.notificationManager.showQualitySuccess(filePath, 'RuboCop'); } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); // RuboCop returns non-zero exit when offenses are found if (output.includes('Offense')) { this.notificationManager.showCriticalQualityError(filePath, 'RuboCop', `${output}\n\nRun 'rubocop -a ${filePath}' to auto-correct fixable offenses.`); } else { this.notificationManager.showCriticalQualityError(filePath, 'RuboCop', output); } } } // Review-specific methods that return results instead of showing notifications async runESLintCheckForReview(filePath) { try { // Run ESLint with context-sensitive configuration await ToolExecutor.runESLint(filePath); return null; // No issues found } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); const issues = this.parseESLintOutput(output); // Determine severity based on output const hasErrors = output.includes('error'); const severity = hasErrors ? 'error' : 'warning'; return { filePath, tool: 'ESLint', severity, issues, raw_output: output, explanation: this.generateESLintExplanation(issues), fixes: this.generateESLintFixes(issues), }; } } async runRuffCheckForReview(filePath) { try { await ToolExecutor.runRuff(filePath, false); // false = no fix, just check return null; // No issues found } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); const issues = this.parseRuffOutput(output); return { filePath, tool: 'Ruff', severity: 'error', issues, }; } } async runGoCheckForReview(filePath) { const issues = []; try { // Check formatting const { stdout: fmtOutput } = await ToolExecutor.runGoFmt(filePath); if (fmtOutput.trim()) { // Get diff to show what needs to change const { stdout: diffOutput } = await execAsync(`gofmt -d "${filePath}"`); issues.push(`Formatting needed - Run 'gofmt -w ${filePath}'`); issues.push('Formatting differences:'); diffOutput .split('\n') .slice(0, 10) .forEach(line => { if (line.startsWith('+') || line.startsWith('-')) { issues.push(` ${line}`); } }); } } catch (error) { issues.push(`gofmt error: ${error.message}`); } try { // Run go vet await ToolExecutor.runGoVet(filePath); } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); issues.push(`go vet: ${output.trim()}`); } return issues.length > 0 ? { filePath, tool: 'Go (gofmt + go vet)', severity: 'error', issues, } : null; } async runRustCheckForReview(filePath) { try { await ToolExecutor.runRustFmt(filePath); await execAsync(`cargo clippy --manifest-path "${path.dirname(filePath)}/Cargo.toml" -- -D warnings`); return null; // No issues found } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); return { filePath, tool: 'Rust (rustfmt + clippy)', severity: 'error', issues: [output.trim()], }; } } async runCSharpCheckForReview(filePath) { try { await ToolExecutor.runDotNetFormat(filePath); return null; // No issues found } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); return { filePath, tool: 'C# (dotnet format)', severity: 'error', issues: [output.trim()], }; } } async runJavaCheckForReview(filePath) { try { await ToolExecutor.runCheckstyle(filePath); return null; // No issues found } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); return { filePath, tool: 'Java (Checkstyle)', severity: 'error', issues: [output.trim()], }; } } async runPHPCheckForReview(filePath) { try { await ToolExecutor.runPhpCs(filePath, 'PSR12'); return null; // No issues found } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); return { filePath, tool: 'PHP (PHPCS)', severity: 'error', issues: [output.trim()], }; } } async runRubyCheckForReview(filePath) { try { await ToolExecutor.runRuboCop(filePath); return null; // No issues found } catch (error) { const output = String(error?.stdout || error?.stderr || error?.message || 'Unknown error'); return { filePath, tool: 'Ruby (RuboCop)', severity: 'error', issues: [output.trim()], }; } } // Helper methods to parse tool outputs parseESLintOutput(output) { const lines = output.split('\n').filter(line => line.trim()); const issues = []; // ESLint output format: file:line:column: severity message (rule) lines.forEach(line => { // Match ESLint output pattern const match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s+([a-z0-9\-/]+)$/); if (match) { const [, lineNum, column, severity, message, rule] = match; issues.push(`Line ${lineNum}:${column} - ${severity.toUpperCase()}: ${message} (Rule: ${rule})`); } else if (line.includes('error') || line.includes('warning')) { // Fallback for non-standard format issues.push(line.trim()); } }); return issues.slice(0, 20); // Increased limit for more detailed output } parseRuffOutput(output) { const lines = output.split('\n').filter(line => line.trim()); const issues = []; // Ruff output format: file.py:line:column: CODE message lines.forEach(line => { const match = line.match(/\.py:(\d+):(\d+):\s+([A-Z0-9]+)\s+(.+)$/); if (match) { const [, lineNum, column, code, message] = match; issues.push(`Line ${lineNum}:${column} - ${message} (Code: ${code})`); } else if (line.includes('.py:') && (line.includes('error') || line.includes('warning'))) { // Fallback for variations issues.push(line.trim()); } }); return issues.slice(0, 20); // Increased limit } /** * Runs Snyk security checks on the provided files and their dependencies * @param filePaths Array of file paths to check * @returns Array of Snyk results containing vulnerabilities and code issues */ async runSnykChecks(filePaths) { const results = []; try { // First, check if Snyk is installed await execAsync('snyk --version'); } catch { return [ { type: 'dependencies', error: 'Snyk is not installed. Run "npm install -g snyk" and "snyk auth" to set up.', }, ]; } // Run dependency vulnerability scan const depResult = await this.runSnykDependencyCheck(); if (depResult) { results.push(depResult); } // Run code security scan on changed files const codeResult = await this.runSnykCodeCheck(filePaths); if (codeResult) { results.push(codeResult); } return results; } /** * Runs Snyk test for dependency vulnerabilities */ async runSnykDependencyCheck() { try { const { stdout } = await execAsync('snyk test --json', { maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs }); const data = JSON.parse(stdout); // Parse vulnerabilities from Snyk output const vulnerabilities = []; const summary = { total: 0, critical: 0, high: 0, medium: 0, low: 0, }; if (data.vulnerabilities && Array.isArray(data.vulnerabilities)) { data.vulnerabilities.forEach((vuln) => { const severity = vuln.severity.toLowerCase(); summary.total++; summary[severity]++; vulnerabilities.push({ id: vuln.id, title: vuln.title, severity, packageName: vuln.packageName, version: vuln.version, from: vuln.from || [], upgradePath: vuln.upgradePath, isUpgradable: vuln.isUpgradable || false, isPatchable: vuln.isPatchable || false, description: vuln.description, fixedIn: vuln.fixedIn, exploit: vuln.exploit, CVSSv3: vuln.CVSSv3, semver: vuln.semver, }); }); } return { type: 'dependencies', vulnerabilities, summary, }; } catch (error) { // If error contains JSON, it might be vulnerabilities found (non-zero exit) const errorWithStdout = error; if (errorWithStdout.stdout) { try { const data = JSON.parse(errorWithStdout.stdout); if (data.vulnerabilities) { const importedResult = this.parseSnykErrorOutput(data); return { type: 'dependencies', vulnerabilities: importedResult .vulnerabilities || [], summary: importedResult.summary || { total: 0, critical: 0, high: 0, medium: 0, low: 0, }, }; } } catch { // Not JSON, actual error } } return { type: 'dependencies', error: `Snyk dependency check failed: ${error.message}`, }; } } /** * Runs Snyk Code for static application security testing */ async runSnykCodeCheck(filePaths) { try { // Snyk Code scans the entire project, not individual files // But we'll filter results to only show issues in changed files const { stdout } = await execAsync('snyk code test --json', { maxBuffer: 10 * 1024 * 1024, // 10MB buffer }); const data = JSON.parse(stdout); const codeIssues = []; const summary = { total: 0, critical: 0, high: 0, medium: 0, low: 0, }; // Filter to only include issues in our changed files const changedFilesSet = new Set(filePaths.map(fp => path.resolve(fp))); if (data.runs && data.runs[0] && data.runs[0].results) { data.runs[0].results.forEach((result) => { if (result.locations && result.locations[0]) { const location = result.locations[0].physicalLocation; const filePath = location.artifactLocation.uri; const absolutePath = path.resolve(filePath); // Only include if it's in our changed files if (changedFilesSet.has(absolutePath)) { const severity = this.mapSnykCodeSeverity(result.level); summary.total++; summary[severity]++; codeIssues.push({ filePath, line: location.region.startLine, column: location.region.startColumn || 0, severity, title: result.message.text, message: result.message.text, ruleId: result.ruleId, categories: result.properties?.categories || [], }); } } }); } return { type: 'code', codeIssues, summary, }; } catch (error) { // Check if Snyk Code is available if (error.message.includes('snyk code is not supported')) { return { type: 'code', error: 'Snyk Code is not available. Ensure you have authenticated with "snyk auth" and have access to Snyk Code.', }; } return { type: 'code', error: `Snyk Code check failed: ${error.message}`, }; } } /** * Helper to parse Snyk error output that contains vulnerability data */ parseSnykErrorOutput(data) { const vulnerabilities = []; const summary = { total: 0, critical: 0, high: 0, medium: 0, low: 0, }; if (data.vulnerabilities && Array.isArray(data.vulnerabilities)) { data.vulnerabilities.forEach((vuln) => { const severity = vuln.severity.toLowerCase(); summary.total++; summary[severity]++; vulnerabilities.push({ id: vuln.id, title: vuln.title, severity, packageName: vuln.packageName, version: vuln.version, from: vuln.from || [], upgradePath: vuln.upgradePath, isUpgradable: vuln.isUpgradable || false, isPatchable: vuln.isPatchable || false, description: vuln.description, fixedIn: vuln.fixedIn, exploit: vuln.exploit, CVSSv3: vuln.CVSSv3, semver: vuln.semver, }); }); } return { type: 'dependencies', tool: 'snyk', totalVulnerabilities: vulnerabilities.length, findings: [], // Will be populated if needed summary, vulnerabilities, }; } /** * Maps Snyk Code severity levels to our standard severity */ mapSnykCodeSeverity(level) { switch (level?.toLowerCase()) { case 'error': case 'critical': return 'critical'; case 'warning': case 'high': return 'high'; case 'note': case 'medium': return 'medium'; default: return 'low'; } } /** * Run comprehensive security checks for code review * Includes Snyk, Gitleaks, and other security tools */ async runSecurityChecksForReview(filePaths, options = {}) { const results = []; // Run Snyk checks (dependencies and code) const snykResults = await this.runSnykChecks(filePaths); results.push(...this.convertSnykToSecurityResults(snykResults)); // Run Gitleaks for secret detection const gitleaksResult = await this.runGitleaksCheck(filePaths, options); if (gitleaksResult) { results.push(gitleaksResult); } // Run basic security analysis for XSS and other vulnerabilities const basicSecurityResult = await this.runBasicSecurityAnalysis(filePaths, options); if (basicSecurityResult) { results.push(basicSecurityResult); } // πŸͺ HOOK: onReportGeneration - KI-freundliche Regelwelt try { await triggerHook('onReportGeneration', { reportType: 'security-analysis', data: results, timestamp: new Date(), }); } catch (hookError) { console.debug(`Hook error (onReportGeneration security): ${hookError}`); } return results; } /** * Run Gitleaks to detect secrets in code */ async runGitleaksCheck(filePaths, _options = {}) { try { // Check if gitleaks is installed await execAsync('which gitleaks'); } catch { console.log(`⚠️ ${i18next.t('security_analysis.gitleaks_not_installed')}`); return null; } const findings = []; const summary = { total: 0, critical: 0, high: 0, medium: 0, low: 0, info: 0, }; try { // Create a temporary file list for gitleaks const tempFile = path.join(process.cwd(), '.woaru-gitleaks-files.txt'); await fs.writeFile(tempFile, filePaths.join('\n')); // Run gitleaks on specific files const { stdout } = await execAsync(`gitleaks detect --source . --report-format json --no-git --files-at-path "${tempFile}" 2>/dev/null || true`, { maxBuffer: 10 * 1024 * 1024 } // 10MB buffer ); // Clean up temp file await fs.remove(tempFile); if (stdout) { const results =