282 lines
13 KiB
PHP
Executable File
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;
|
|
}
|
|
}
|