Files
wevia-brain/modules/advanced/code-analyzer.php
2026-04-12 23:01:36 +02:00

282 lines
13 KiB
PHP
Executable File

<?php
/**
* WEVIA OPUS — Code Analyzer
*
* Analyse statique de code avant de le proposer à l'utilisateur.
* Vérifie : syntaxe, sécurité, performance, qualité, patterns.
*
* C'est le "quality gate" d'Opus — tout code passe par là avant livraison.
*/
class CodeAnalyzer {
private string $ollamaUrl;
public function __construct(string $ollamaUrl = 'http://localhost:11434') {
$this->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' => ['<?php', '<?=', 'function ', '$this->', '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' => ['<html', '<div', '<body', '<head', '<!DOCTYPE'],
'css' => ['{', '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;
}
}