ollamaUrl = $ollamaUrl; } /** * Analyse complète d'un bloc de code */ public function analyze(string $code, string $language = 'auto'): array { if ($language === 'auto') { $language = $this->detectLanguage($code); } $results = [ 'language' => $language, 'lines' => substr_count($code, "\n") + 1, 'characters' => strlen($code), 'security' => $this->checkSecurity($code, $language), 'quality' => $this->checkQuality($code, $language), 'performance' => $this->checkPerformance($code, $language), 'patterns' => $this->detectPatterns($code, $language), 'score' => 0 ]; // Score global $securityScore = $results['security']['score'] ?? 10; $qualityScore = $results['quality']['score'] ?? 10; $perfScore = $results['performance']['score'] ?? 10; $results['score'] = round(($securityScore * 0.4 + $qualityScore * 0.3 + $perfScore * 0.3), 1); $results['verdict'] = $results['score'] >= 7 ? 'PASS' : ($results['score'] >= 4 ? 'WARNING' : 'FAIL'); return $results; } /** * Détecte le langage du code */ private function detectLanguage(string $code): string { $indicators = [ 'php' => ['', 'namespace ', 'use '], 'python' => ['def ', 'import ', 'from ', 'class ', 'self.', 'print(', 'if __name__'], 'javascript' => ['const ', 'let ', 'var ', '=>', 'function(', 'document.', 'require(', 'export '], 'sql' => ['SELECT ', 'INSERT ', 'UPDATE ', 'DELETE ', 'CREATE TABLE', 'ALTER TABLE', 'WHERE '], 'bash' => ['#!/bin/bash', 'echo ', 'if [', 'then', 'fi', 'done', 'apt ', 'systemctl '], 'html' => [' ['{', 'margin:', 'padding:', 'display:', 'color:', '@media'], ]; $scores = []; foreach ($indicators as $lang => $keywords) { $score = 0; foreach ($keywords as $kw) { if (stripos($code, $kw) !== false) $score++; } $scores[$lang] = $score; } arsort($scores); $topLang = array_key_first($scores); return ($scores[$topLang] > 0) ? $topLang : 'unknown'; } /** * Vérifie les vulnérabilités de sécurité */ private function checkSecurity(string $code, string $language): array { $issues = []; // Vérifications universelles $sensitivePatterns = [ '/password\s*=\s*["\'][^"\']+["\']/' => 'Mot de passe en dur dans le code', '/api[_-]?key\s*=\s*["\'][^"\']+["\']/' => 'Clé API en dur', '/secret\s*=\s*["\'][^"\']+["\']/' => 'Secret en dur', '/token\s*=\s*["\'][A-Za-z0-9]{20,}["\']/' => 'Token en dur', ]; foreach ($sensitivePatterns as $pattern => $message) { if (preg_match($pattern, $code)) { $issues[] = ['severity' => 'CRITICAL', 'message' => $message, 'category' => 'credentials']; } } // PHP spécifique if ($language === 'php') { $phpChecks = [ '/\$_(GET|POST|REQUEST|COOKIE)\s*\[.*\]/' => ['message' => 'Input utilisateur non validé', 'severity' => 'HIGH', 'check_context' => true], '/eval\s*\(/' => ['message' => 'Usage de eval() — injection de code possible', 'severity' => 'CRITICAL'], '/exec\s*\(|system\s*\(|passthru\s*\(|shell_exec\s*\(|popen\s*\(/' => ['message' => 'Exécution de commande système', 'severity' => 'HIGH'], '/mysql_query\s*\(/' => ['message' => 'mysql_* obsolète — utiliser PDO avec requêtes paramétrées', 'severity' => 'HIGH'], '/\$\w+\s*\.\s*\$_(GET|POST)/' => ['message' => 'Concaténation SQL possible — utiliser des requêtes paramétrées', 'severity' => 'CRITICAL'], '/".*\$_(GET|POST|REQUEST).*"/' => ['message' => 'Interpolation de variables user dans string (possible XSS/SQLi)', 'severity' => 'HIGH'], '/header\s*\(\s*["\']Location.*\$/' => ['message' => 'Redirect avec input non validé (Open Redirect)', 'severity' => 'MEDIUM'], '/unserialize\s*\(/' => ['message' => 'unserialize() sur données non fiables = Remote Code Execution', 'severity' => 'CRITICAL'], '/md5\s*\(|sha1\s*\(/' => ['message' => 'Hash faible — utiliser password_hash()/password_verify()', 'severity' => 'MEDIUM'], ]; foreach ($phpChecks as $pattern => $check) { if (preg_match($pattern, $code)) { $issues[] = ['severity' => $check['severity'], 'message' => $check['message'], 'category' => 'code_security']; } } } // Python spécifique if ($language === 'python') { $pyChecks = [ '/pickle\.load/' => ['message' => 'pickle.load() sur données non fiables = RCE', 'severity' => 'CRITICAL'], '/eval\s*\(/' => ['message' => 'eval() — injection possible', 'severity' => 'CRITICAL'], '/os\.system\s*\(/' => ['message' => 'os.system() — préférer subprocess.run()', 'severity' => 'MEDIUM'], '/\.format\(.*input/' => ['message' => 'f-string/format avec user input possible', 'severity' => 'MEDIUM'], '/verify\s*=\s*False/' => ['message' => 'SSL verification désactivée', 'severity' => 'HIGH'], ]; foreach ($pyChecks as $pattern => $check) { if (preg_match($pattern, $code)) { $issues[] = ['severity' => $check['severity'], 'message' => $check['message'], 'category' => 'code_security']; } } } // SQL spécifique if ($language === 'sql') { if (preg_match('/DROP\s+TABLE|DROP\s+DATABASE|TRUNCATE/i', $code)) { $issues[] = ['severity' => 'HIGH', 'message' => 'Commande destructive détectée', 'category' => 'data_safety']; } if (preg_match('/GRANT\s+ALL|GRANT.*WITH\s+GRANT/i', $code)) { $issues[] = ['severity' => 'MEDIUM', 'message' => 'Permissions excessives', 'category' => 'access_control']; } } $criticalCount = count(array_filter($issues, fn($i) => $i['severity'] === 'CRITICAL')); $highCount = count(array_filter($issues, fn($i) => $i['severity'] === 'HIGH')); $score = max(0, 10 - ($criticalCount * 4) - ($highCount * 2) - (count($issues) * 0.5)); return [ 'issues' => $issues, 'issue_count' => count($issues), 'critical_count' => $criticalCount, 'score' => round($score, 1) ]; } /** * Vérifie la qualité du code */ private function checkQuality(string $code, string $language): array { $issues = []; $lines = explode("\n", $code); $lineCount = count($lines); // Lignes trop longues $longLines = 0; foreach ($lines as $i => $line) { if (strlen($line) > 120) { $longLines++; if ($longLines <= 3) { $issues[] = ['severity' => 'LOW', 'message' => "Ligne " . ($i+1) . " dépasse 120 caractères (" . strlen($line) . ")", 'category' => 'formatting']; } } } // TODO/FIXME/HACK if (preg_match_all('/(TODO|FIXME|HACK|XXX|TEMP)/i', $code, $matches)) { $issues[] = ['severity' => 'LOW', 'message' => count($matches[0]) . " TODO/FIXME trouvés", 'category' => 'technical_debt']; } // Code dupliqué (basique) $lineHashes = []; $duplicates = 0; foreach ($lines as $line) { $trimmed = trim($line); if (strlen($trimmed) > 20) { $hash = md5($trimmed); if (isset($lineHashes[$hash])) $duplicates++; $lineHashes[$hash] = true; } } if ($duplicates > 3) { $issues[] = ['severity' => 'MEDIUM', 'message' => "$duplicates lignes dupliquées détectées", 'category' => 'duplication']; } // Fonctions trop longues (PHP/Python/JS) if (in_array($language, ['php', 'python', 'javascript'])) { $functionPattern = $language === 'python' ? '/def \w+/' : '/function\s+\w+|=>\s*{/'; $functionCount = preg_match_all($functionPattern, $code); if ($functionCount > 0 && $lineCount / $functionCount > 50) { $issues[] = ['severity' => 'MEDIUM', 'message' => "Fonctions potentiellement trop longues (moy: " . round($lineCount / $functionCount) . " lignes)", 'category' => 'complexity']; } } // Error handling if ($language === 'php' && !preg_match('/try\s*{|catch\s*\(/', $code) && $lineCount > 20) { $issues[] = ['severity' => 'MEDIUM', 'message' => "Pas de gestion d'erreurs (try/catch) dans un code de " . $lineCount . " lignes", 'category' => 'error_handling']; } if ($language === 'python' && !preg_match('/try:|except/', $code) && $lineCount > 20) { $issues[] = ['severity' => 'MEDIUM', 'message' => "Pas de gestion d'erreurs (try/except)", 'category' => 'error_handling']; } $score = max(0, 10 - count(array_filter($issues, fn($i) => $i['severity'] === 'MEDIUM')) * 1.5 - count($issues) * 0.3); return [ 'issues' => $issues, 'metrics' => [ 'lines' => $lineCount, 'long_lines' => $longLines, 'duplicates' => $duplicates, ], 'score' => round($score, 1) ]; } /** * Vérifie les problèmes de performance */ private function checkPerformance(string $code, string $language): array { $issues = []; // SQL N+1 if ($language === 'php' && preg_match('/while.*fetch.*SELECT/is', $code)) { $issues[] = ['severity' => 'HIGH', 'message' => "Possible N+1 query (SELECT dans une boucle)", 'category' => 'n_plus_1']; } // Chargement de données sans LIMIT if ($language === 'sql' && preg_match('/SELECT.*FROM/i', $code) && !preg_match('/LIMIT/i', $code)) { $issues[] = ['severity' => 'MEDIUM', 'message' => "SELECT sans LIMIT — peut charger des millions de lignes", 'category' => 'unbounded_query']; } // String concatenation dans boucle (PHP) if ($language === 'php' && preg_match('/for.*\.=\s*["\']|while.*\.=\s*["\']/', $code)) { $issues[] = ['severity' => 'LOW', 'message' => "Concaténation de string dans boucle — utiliser implode() ou output buffering", 'category' => 'string_concat']; } // Large file sans streaming if (preg_match('/file_get_contents|readfile.*csv|fread.*entire/', $code)) { $issues[] = ['severity' => 'MEDIUM', 'message' => "Lecture de fichier entier en mémoire — considérer le streaming", 'category' => 'memory']; } $score = max(0, 10 - count(array_filter($issues, fn($i) => $i['severity'] === 'HIGH')) * 3 - count($issues) * 0.5); return [ 'issues' => $issues, 'score' => round($score, 1) ]; } /** * Détecte les patterns et anti-patterns */ private function detectPatterns(string $code, string $language): array { $patterns = []; // Design patterns positifs if (preg_match('/class\s+\w+\s+implements/', $code)) $patterns[] = ['type' => 'positive', 'pattern' => 'Interface implementation']; if (preg_match('/private\s+static\s+\$instance/', $code)) $patterns[] = ['type' => 'info', 'pattern' => 'Singleton']; if (preg_match('/class\s+\w+Factory/', $code)) $patterns[] = ['type' => 'positive', 'pattern' => 'Factory']; if (preg_match('/new\s+\w+Strategy/', $code)) $patterns[] = ['type' => 'positive', 'pattern' => 'Strategy']; // Anti-patterns if (preg_match('/god.*class|class.*god/i', $code) || (substr_count($code, 'function') > 20 && substr_count($code, 'class') === 1)) { $patterns[] = ['type' => 'negative', 'pattern' => 'God Class (trop de responsabilités)']; } if (preg_match('/catch\s*\(\s*(Exception|\\\Exception)\s*\$\w+\s*\)\s*\{\s*\}/', $code)) { $patterns[] = ['type' => 'negative', 'pattern' => 'Empty catch block (exceptions avalées)']; } return $patterns; } }