219 lines
9.5 KiB
PHP
Executable File
219 lines
9.5 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* WEVAL MIND SENTINEL — Autonomous Scanner & Repairer
|
|
* Scans, detects, fixes, and learns from infrastructure issues
|
|
*/
|
|
header('Content-Type: application/json');
|
|
error_reporting(0);
|
|
|
|
$start = microtime(true);
|
|
$action = $_GET['action'] ?? $_POST['action'] ?? 'status';
|
|
|
|
try {
|
|
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
} catch (Exception $e) {
|
|
echo json_encode(['error' => 'DB: ' . $e->getMessage()]); exit;
|
|
}
|
|
|
|
$PATHS = [
|
|
'adx' => '/opt/wevads/public',
|
|
'arsenal' => '/opt/wevads-arsenal/public',
|
|
'arsenal_api' => '/opt/wevads-arsenal/public/api'
|
|
];
|
|
$PORTS = ['adx' => 5821, 'arsenal' => 5890, 'fmg' => 5822, 'bcg' => 5823, 'dkim' => 5824];
|
|
|
|
function runFullScan($pdo, $paths, $autoFix = true) {
|
|
global $start, $PORTS;
|
|
$scanId = startScan($pdo, 'full');
|
|
$issues = [];
|
|
$fixed = 0;
|
|
$totalFiles = 0;
|
|
|
|
foreach ($paths as $name => $path) {
|
|
if (!is_dir($path)) continue;
|
|
|
|
// 1. PHP Syntax Check
|
|
$phpFiles = glob("$path/*.php");
|
|
foreach ($phpFiles as $f) {
|
|
$totalFiles++;
|
|
$out = shell_exec("php -l " . escapeshellarg($f) . " 2>&1");
|
|
if (strpos($out, 'Parse error') !== false || strpos($out, 'Fatal') !== false) {
|
|
$issues[] = ['file' => basename($f), 'path' => $f, 'type' => 'php-syntax-error', 'detail' => substr($out, 0, 200)];
|
|
logFix($pdo, $scanId, $f, 'php-syntax-error', 'Logged for review');
|
|
}
|
|
}
|
|
|
|
// 2. HTML Script Tag Balance
|
|
$htmlFiles = glob("$path/*.html");
|
|
foreach ($htmlFiles as $f) {
|
|
$totalFiles++;
|
|
$content = @file_get_contents($f);
|
|
if (!$content) continue;
|
|
|
|
$opens = substr_count($content, '<script');
|
|
$closes = substr_count($content, '</script>');
|
|
if ($opens > $closes) {
|
|
$diff = $opens - $closes;
|
|
$issues[] = ['file' => basename($f), 'path' => $f, 'type' => 'unclosed-script', 'detail' => "open=$opens close=$closes"];
|
|
if ($autoFix && is_writable($f)) {
|
|
file_put_contents($f, $content . str_repeat("</script>\n", $diff));
|
|
$fixed++;
|
|
logFix($pdo, $scanId, $f, 'unclosed-script', "Appended $diff </script>");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Injection Detection
|
|
$allFiles = array_merge($phpFiles, $htmlFiles);
|
|
foreach ($allFiles as $f) {
|
|
$content = @file_get_contents($f);
|
|
if (!$content) continue;
|
|
|
|
// WEVADS_NUKE
|
|
if (strpos($content, "WEVADS_" . "NUKE") !== false && strpos($f, "sentinel") === false) {
|
|
$issues[] = ['file' => basename($f), 'path' => $f, 'type' => 'WEVADS_NUKE', 'detail' => 'Malicious injection'];
|
|
if ($autoFix && is_writable($f)) {
|
|
$content = preg_replace('/<style id="WEVADS_NUKE">[^<]*<\/style>/s', '', $content);
|
|
$content = preg_replace('/<script id="WEVADS_KILL_JS">.*?<\/script>/s', '', $content);
|
|
file_put_contents($f, $content);
|
|
$fixed++;
|
|
logFix($pdo, $scanId, $f, 'WEVADS_NUKE', 'Removed NUKE blocks');
|
|
}
|
|
}
|
|
|
|
// weval-theme-system-v2
|
|
if (strpos($content, 'weval-theme-system-v2') !== false) {
|
|
$issues[] = ['file' => basename($f), 'path' => $f, 'type' => 'weval-theme-v2', 'detail' => 'Theme injection'];
|
|
if ($autoFix && is_writable($f)) {
|
|
$content = preg_replace('/<!-- weval-theme-system-v2 -->.*?<!-- \/weval-theme-system-v2 -->/s', '', $content);
|
|
file_put_contents($f, $content);
|
|
$fixed++;
|
|
logFix($pdo, $scanId, $f, 'weval-theme-v2', 'Removed theme block');
|
|
}
|
|
}
|
|
|
|
// Broken </script> in URLs
|
|
if (preg_match('/action=[^>]*<\/script>[a-z]*=/', $content)) {
|
|
$issues[] = ['file' => basename($f), 'path' => $f, 'type' => 'broken-script-url', 'detail' => '</script> in URL'];
|
|
if ($autoFix && is_writable($f)) {
|
|
$content = preg_replace('/(action=[^>]*)<\/script>([a-z]*=)/', '$1&$2', $content);
|
|
file_put_contents($f, $content);
|
|
$fixed++;
|
|
logFix($pdo, $scanId, $f, 'broken-script-url', 'Fixed URL');
|
|
}
|
|
}
|
|
|
|
// Content after </body></html>
|
|
$pos = strpos($content, '</body></html>');
|
|
if ($pos !== false) {
|
|
$after = trim(substr($content, $pos + 14));
|
|
if (strlen($after) > 20) {
|
|
$issues[] = ['file' => basename($f), 'path' => $f, 'type' => 'content-after-html', 'detail' => 'Junk after </html>'];
|
|
if ($autoFix && is_writable($f)) {
|
|
file_put_contents($f, substr($content, 0, $pos + 14) . "\n");
|
|
$fixed++;
|
|
logFix($pdo, $scanId, $f, 'content-after-html', 'Truncated after </html>');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Port Health
|
|
foreach ($PORTS as $name => $port) {
|
|
$ch = curl_init("http://localhost:$port/");
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3, CURLOPT_NOBODY => true]);
|
|
curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
if ($code == 0) {
|
|
$issues[] = ['file' => "port:$port", 'path' => '', 'type' => 'port-down', 'detail' => "$name unreachable"];
|
|
}
|
|
}
|
|
|
|
// 5. Runtime Errors (sample top 50 PHP pages)
|
|
$phpPages = array_slice(glob($paths['adx'] . '/*.php'), 0, 50);
|
|
foreach ($phpPages as $f) {
|
|
$bn = basename($f);
|
|
if (in_array($bn, ['claude-exec.php','claude-exec2.php','index.php'])) continue;
|
|
$ch = curl_init("http://localhost:5821/$bn");
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3]);
|
|
$out = curl_exec($ch);
|
|
curl_close($ch);
|
|
if ($out && preg_match('/<b>(Warning|Fatal error|Parse error)<\/b>/', $out)) {
|
|
$issues[] = ['file' => $bn, 'path' => $f, 'type' => 'runtime-error', 'detail' => 'PHP warning/error on load'];
|
|
}
|
|
}
|
|
|
|
$duration = round((microtime(true) - $start) * 1000);
|
|
$score = $totalFiles > 0 ? round((1 - count($issues) / max($totalFiles, 1)) * 100, 1) : 100;
|
|
|
|
finishScan($pdo, $scanId, $totalFiles, count($issues), $fixed, $score, $duration);
|
|
|
|
// Update pattern stats
|
|
foreach ($issues as $i) {
|
|
$pdo->prepare("UPDATE admin.sentinel_patterns SET times_detected = times_detected + 1, last_seen = NOW() WHERE pattern_name = ?")->execute([$i['type']]);
|
|
}
|
|
|
|
return [
|
|
'scan_id' => $scanId,
|
|
'total_files' => $totalFiles,
|
|
'issues_found' => count($issues),
|
|
'issues_fixed' => $fixed,
|
|
'score' => $score,
|
|
'duration_ms' => $duration,
|
|
'issues' => $issues
|
|
];
|
|
}
|
|
|
|
function startScan($pdo, $type) {
|
|
$stmt = $pdo->prepare("INSERT INTO admin.sentinel_scans (scan_type) VALUES (?) RETURNING id");
|
|
$stmt->execute([$type]);
|
|
return $stmt->fetchColumn();
|
|
}
|
|
|
|
function finishScan($pdo, $id, $total, $issues, $fixed, $score, $duration) {
|
|
$pdo->prepare("UPDATE admin.sentinel_scans SET total_files=?, issues_found=?, issues_fixed=?, score=?, duration_ms=? WHERE id=?")->execute([$total, $issues, $fixed, $score, $duration, $id]);
|
|
}
|
|
|
|
function logFix($pdo, $scanId, $file, $type, $fix) {
|
|
$pdo->prepare("INSERT INTO admin.sentinel_fixes (file_path, issue_type, fix_applied, scan_id) VALUES (?,?,?,?)")->execute([$file, $type, $fix, $scanId]);
|
|
}
|
|
|
|
// API ROUTES
|
|
switch ($action) {
|
|
case 'scan':
|
|
$autoFix = ($_GET['fix'] ?? '1') === '1';
|
|
echo json_encode(runFullScan($pdo, $PATHS, $autoFix), JSON_PRETTY_PRINT);
|
|
break;
|
|
|
|
case 'status':
|
|
$last = $pdo->query("SELECT * FROM admin.sentinel_scans ORDER BY scan_date DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
|
$totalScans = $pdo->query("SELECT count(*) FROM admin.sentinel_scans")->fetchColumn();
|
|
$totalFixes = $pdo->query("SELECT count(*) FROM admin.sentinel_fixes")->fetchColumn();
|
|
$patterns = $pdo->query("SELECT pattern_name, times_detected, times_fixed, severity FROM admin.sentinel_patterns ORDER BY times_detected DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['status' => 'operational', 'last_scan' => $last, 'total_scans' => (int)$totalScans, 'total_fixes' => (int)$totalFixes, 'patterns' => $patterns], JSON_PRETTY_PRINT);
|
|
break;
|
|
|
|
case 'history':
|
|
$limit = min((int)($_GET['limit'] ?? 20), 100);
|
|
$scans = $pdo->query("SELECT * FROM admin.sentinel_scans ORDER BY scan_date DESC LIMIT $limit")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['scans' => $scans]);
|
|
break;
|
|
|
|
case 'fixes':
|
|
$limit = min((int)($_GET['limit'] ?? 50), 200);
|
|
$fixes = $pdo->query("SELECT * FROM admin.sentinel_fixes ORDER BY fix_date DESC LIMIT $limit")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['fixes' => $fixes]);
|
|
break;
|
|
|
|
case 'patterns':
|
|
$patterns = $pdo->query("SELECT * FROM admin.sentinel_patterns ORDER BY times_detected DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['patterns' => $patterns]);
|
|
break;
|
|
|
|
default:
|
|
echo json_encode(['error' => 'Unknown action', 'available' => ['scan','status','history','fixes','patterns']]);
|
|
}
|