woaru
Version:
Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language
1,232 lines β’ 75.9 kB
JavaScript
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 =