Files
html/api/architecture-recommendations.php
2026-04-16 02:28:32 +02:00

198 lines
11 KiB
PHP

<?php
// WEVAL ARCHITECTURE RECOMMENDATIONS ENGINE v1.0
// Analyzes scan data, generates recommendations, auto-fixes when possible
// Called by architecture-scanner.php after scan completes
function generate_recommendations($A) {
$recs = [];
$auto_fixes = [];
$ts = date('Y-m-d H:i:s');
// ═══ CATEGORY 1: INFRASTRUCTURE HEALTH ═══
foreach ($A['servers'] as $s) {
$disk = $s['disk_pct'] ?? 0;
if ($disk >= 95) {
$recs[] = ['severity'=>'critical','category'=>'INFRA','title'=>"{$s['id']}: Disk {$disk}% CRITICAL",
'detail'=>"Espace disque critique. Risque de crash imminent. Nettoyer logs, truncate syslog, docker prune.",
'action'=>'auto','fix_cmd'=>"truncate -s 0 /var/log/syslog.1 /var/log/crowdsec-firewall-bouncer.log; journalctl --vacuum-size=100M; docker system prune -f --volumes 2>/dev/null"];
} elseif ($disk >= 92) {
$recs[] = ['severity'=>'warning','category'=>'INFRA','title'=>"{$s['id']}: Disk {$disk}% élevé",
'detail'=>"Espace disque > 90%. Prévoir nettoyage. Vérifier /var/log, Docker images, old backups.",
'action'=>'auto','fix_cmd'=>"find /var/log -name '*.gz' -delete; find /var/log -name '*.1' -size +10M -exec truncate -s 0 {} +; journalctl --vacuum-size=200M; docker image prune -af; pip cache purge 2>/dev/null"];
}
if (isset($s['nginx']) && $s['nginx'] !== 'active') {
$recs[] = ['severity'=>'critical','category'=>'INFRA','title'=>"{$s['id']}: Nginx DOWN",
'detail'=>"Nginx inactif. Site inaccessible. Restart immédiat.",
'action'=>'auto','fix_cmd'=>"systemctl restart nginx"];
}
if (isset($s['php_fpm']) && $s['php_fpm'] !== 'active') {
$recs[] = ['severity'=>'critical','category'=>'INFRA','title'=>"{$s['id']}: PHP-FPM DOWN",
'detail'=>"PHP-FPM inactif. APIs inaccessibles. Restart immédiat.",
'action'=>'auto','fix_cmd'=>"systemctl restart php8.5-fpm"];
}
}
// ═══ CATEGORY 2: DOCKER HEALTH ═══
$unhealthy = 0;
$restarting = 0;
foreach ($A['docker'] as $c) {
$st = strtolower($c['status'] ?? '');
if (strpos($st, 'unhealthy') !== false) {
$unhealthy++;
$recs[] = ['severity'=>'warning','category'=>'DOCKER','title'=>"Container {$c['name']} unhealthy",
'detail'=>"Container en état unhealthy. Vérifier logs: docker logs {$c['name']} --tail 20",
'action'=>'manual','fix_cmd'=>"docker restart {$c['name']}"];
}
if (strpos($st, 'restarting') !== false) {
$restarting++;
$recs[] = ['severity'=>'critical','category'=>'DOCKER','title'=>"Container {$c['name']} en restart loop",
'detail'=>"Container redémarre en boucle. Stopper + investiguer.",
'action'=>'auto','fix_cmd'=>"docker update --restart=no {$c['name']}; docker stop {$c['name']}"];
}
}
// ═══ CATEGORY 3: AUTHENTICATION / SECURITY ═══
foreach ($A['domains'] as $dm) {
if ($dm['authentik'] && !$dm['authentik_paths']) {
$recs[] = ['severity'=>'critical','category'=>'SECURITY','title'=>"Domaine {$dm['file']}: Authentik paths manquants",
'detail'=>"Outpost configuré mais /application/ /flows/ /if/ non proxiés. SSO callback 400.",
'action'=>'manual','fix_cmd'=>''];
}
if (!$dm['ssl'] && !in_array('localhost', $dm['server_names'] ?? [])) {
$recs[] = ['severity'=>'warning','category'=>'SECURITY','title'=>"Domaine {$dm['file']}: pas de SSL",
'detail'=>"Trafic en clair. Ajouter certificat SSL.",
'action'=>'manual','fix_cmd'=>''];
}
}
// ═══ CATEGORY 4: DATABASE PERFORMANCE ═══
$kt = $A['databases']['key_tables'] ?? [];
if (($kt['ethica_medecins'] ?? 0) > 100000) {
$recs[] = ['severity'=>'info','category'=>'SCALABILITY','title'=>"Ethica HCP: " . number_format($kt['ethica_medecins']) . " records",
'detail'=>"Table volumineuse. Vérifier index, partitioning, VACUUM ANALYZE.",
'action'=>'auto','fix_cmd'=>"PGPASSWORD=admin123 psql -U admin -d adx_system -c 'VACUUM ANALYZE ethica.medecins_validated'"];
}
// ═══ CATEGORY 5: AI / LLM OPTIMIZATION ═══
$ollama_count = count($A['ollama'] ?? []);
$total_size_gb = array_sum(array_column($A['ollama'] ?? [], 'size_gb'));
if ($total_size_gb > 50) {
$recs[] = ['severity'=>'info','category'=>'SCALABILITY','title'=>"Ollama: {$total_size_gb}GB de modèles",
'detail'=>"Espace modèles important. Considérer supprimer modèles non utilisés.",
'action'=>'auto','fix_cmd'=>'curl -s -X DELETE http://127.0.0.1:11434/api/delete -d {"name":"weval-brain-v2:latest"} 2>/dev/null; curl -s -X DELETE http://127.0.0.1:11434/api/delete -d {"name":"qwen2.5:7b"} 2>/dev/null; curl -s -X DELETE http://127.0.0.1:11434/api/delete -d {"name":"mistral:latest"} 2>/dev/null'];
}
if ($ollama_count >= 20) {
$recs[] = ['severity'=>'opportunity','category'=>'OPTIMIZATION','title'=>"Ollama: {$ollama_count} modèles chargés",
'detail'=>"Beaucoup de modèles. Fine-tuner weval-brain-v3 comme modèle unique remplaçant les autres.",
'action'=>'auto','fix_cmd'=>'curl -s -X DELETE http://127.0.0.1:11434/api/delete -d {"name":"weval-brain-v2:latest"} 2>/dev/null'];
}
// Qdrant health
$total_vectors = array_sum(array_column($A['qdrant'] ?? [], 'vectors'));
if ($total_vectors > 20000) {
$recs[] = ['severity'=>'opportunity','category'=>'SCALABILITY','title'=>"Qdrant: " . number_format($total_vectors) . " vecteurs",
'detail'=>"Volume vectoriel croissant. Planifier sharding ou migration vers cluster Qdrant.",
'action'=>'opportunity','fix_cmd'=>''];
}
// ═══ CATEGORY 10: MTA HEALTH (S95) ═══
$s95_ports = sentinel("ss -tln");
if (strpos($s95_ports, ":587 ") === false) {
$recs[] = ['severity'=>'critical','category'=>'INFRA','title'=>'S95: KumoMTA DOWN (:587)',
'detail'=>'KumoMTA non détecté sur port 587. Auto-restart.',
'action'=>'auto','fix_cmd'=>'curl -sf "http://10.1.0.3:5890/api/sentinel-brain.php?action=exec&cmd=sudo%20systemctl%20start%20kumomta" --max-time 5'];
}
if (strpos($s95_ports, ":25 ") === false) {
$recs[] = ['severity'=>'critical','category'=>'INFRA','title'=>'S95: PMTA DOWN (:25)',
'detail'=>'PMTA non détecté sur port 25. Auto-restart via pmtawatch.',
'action'=>'auto','fix_cmd'=>'curl -sf "http://10.1.0.3:5890/api/sentinel-brain.php?action=exec&cmd=sudo%20/usr/sbin/pmtad" --max-time 5'];
}
// ═══ CATEGORY 6: CRON OPTIMIZATION ═══
$cron_total = $A['crons']['s204_total'] ?? 0;
if ($cron_total > 200) {
$recs[] = ['severity'=>'info','category'=>'OPTIMIZATION','title'=>"{$cron_total} crons actifs sur S204",
'detail'=>"Nombre élevé de crons. Consolider les tâches similaires, éviter chevauchements.",
'action'=>'monitor','fix_cmd'=>''];
}
// ═══ CATEGORY 7: L99 QUALITY ═══
$l99_fail = $A['l99']['master']['fail'] ?? 0;
$l99_auth_fail = $A['l99']['auth']['fail'] ?? 0;
if ($l99_fail > 5) {
$recs[] = ['severity'=>'warning','category'=>'QUALITY','title'=>"L99 Master: {$l99_fail} échecs",
'detail'=>"Tests en échec. Investiguer les régressions.",
'action'=>'monitor','fix_cmd'=>''];
}
if ($l99_auth_fail > 0) {
$recs[] = ['severity'=>'critical','category'=>'SECURITY','title'=>"L99 Auth: {$l99_auth_fail} échecs",
'detail'=>"Tests d'authentification en échec. SSO potentiellement cassé.",
'action'=>'manual','fix_cmd'=>''];
}
// ═══ CATEGORY 8: OPPORTUNITIES ═══
$app_count = count($A['applications'] ?? []);
$public_apps = array_filter($A['applications'] ?? [], fn($a) => $a['auth'] === 'public');
$internal_apps = array_filter($A['applications'] ?? [], fn($a) => $a['auth'] === 'internal');
if (count($internal_apps) > 15) {
$recs[] = ['severity'=>'info','category'=>'SECURITY','title'=>count($internal_apps) . " apps internes avec auth PHP basique",
'detail'=>"Migrer progressivement les apps internes (SearXNG, Qdrant UI, Vaultwarden) derrière PHP session auth.",
'action'=>'opportunity','fix_cmd'=>''];
}
// Wiki growth
$wiki_total = $A['wiki']['total_entries'] ?? 0;
if ($wiki_total < 250) {
$recs[] = ['severity'=>'opportunity','category'=>'KNOWLEDGE','title'=>"Wiki: {$wiki_total} entrées",
'detail'=>"Enrichir le KB: documenter chaque nouveau déploiement, incident, décision technique.",
'action'=>'opportunity','fix_cmd'=>''];
}
// ═══ CATEGORY 9: SCALABILITY SCORE ═══
$score = 100;
foreach ($recs as $r) {
if ($r['severity'] === 'critical') $score -= 15;
if ($r['severity'] === 'warning') $score -= 5;
if ($r['severity'] === 'info') $score -= 2;
}
$score = max(0, $score);
// ═══ AUTO-FIX: Execute safe fixes ═══
$fixes_applied = [];
foreach ($recs as &$rec) {
if ($rec['action'] === 'auto' && !empty($rec['fix_cmd'])) {
$output = shell_exec("timeout 10 " . $rec['fix_cmd'] . " 2>&1");
$rec['auto_fixed'] = true;
$rec['fix_output'] = substr(trim($output ?? ''), 0, 200);
$fixes_applied[] = ['title' => $rec['title'], 'cmd' => $rec['fix_cmd'], 'output' => $rec['fix_output'], 'time' => $ts];
}
}
unset($rec);
// Log fixes to KB
if (!empty($fixes_applied)) {
$c = @pg_connect("host=127.0.0.1 dbname=adx_system user=admin password=admin123");
if ($c) {
$fact = "AUTO-FIX " . date('dMY H:i') . ": " . count($fixes_applied) . " fixes applied. " .
implode('; ', array_map(fn($f) => $f['title'], $fixes_applied));
$fact = substr($fact, 0, 500);
@pg_query($c, "INSERT INTO kb_learnings (category,fact,source,confidence,created_at) VALUES ('AUTO-FIX','" . pg_escape_string($c, $fact) . "','arch-scanner',0.9,NOW())");
pg_close($c);
}
}
return [
'score' => $score,
'total' => count($recs),
'critical' => count(array_filter($recs, fn($r) => $r['severity'] === 'critical')),
'warning' => count(array_filter($recs, fn($r) => $r['severity'] === 'warning')),
'info' => count(array_filter($recs, fn($r) => $r['severity'] === 'info')),
'opportunity' => count(array_filter($recs, fn($r) => $r['severity'] === 'opportunity')),
'auto_fixed' => count($fixes_applied),
'fixes_log' => $fixes_applied,
'recommendations' => $recs,
];
}