198 lines
11 KiB
PHP
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 >= 85) {
|
|
$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 > 25) {
|
|
$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:11435/api/delete -d {"name":"weval-brain-v2:latest"} 2>/dev/null; curl -s -X DELETE http://127.0.0.1:11435/api/delete -d {"name":"qwen2.5:7b"} 2>/dev/null; curl -s -X DELETE http://127.0.0.1:11435/api/delete -d {"name":"mistral:latest"} 2>/dev/null'];
|
|
}
|
|
if ($ollama_count >= 8) {
|
|
$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:11435/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 > 40) {
|
|
$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) > 5) {
|
|
$recs[] = ['severity'=>'opportunity','category'=>'SECURITY','title'=>count($internal_apps) . " apps internes sans auth avancee",
|
|
'detail'=>"Migrer progressivement les apps internes (SearXNG, Qdrant UI, Vaultwarden) derrière Authentik Forward 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,
|
|
];
|
|
}
|