361 lines
14 KiB
PHP
361 lines
14 KiB
PHP
<?php
|
|
// เปิดโชว์ Error ชั่วคราวเพื่อหาสาเหตุ 500
|
|
ini_set('display_errors', 1);
|
|
ini_set('display_startup_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use Bramus\Router\Router;
|
|
use Dotenv\Dotenv;
|
|
use App\Controllers\DashboardController;
|
|
use App\Controllers\WazuhWebhookController;
|
|
|
|
// Load environment variables
|
|
if (file_exists(__DIR__ . '/../.env')) {
|
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
|
$dotenv->load();
|
|
}
|
|
|
|
require_once __DIR__ . '/../src/Engine/Language.php';
|
|
|
|
// Handle Language Switch
|
|
if (isset($_GET['lang'])) {
|
|
$lang = in_array($_GET['lang'], ['en', 'th']) ? $_GET['lang'] : 'en';
|
|
setcookie('lang', $lang, time() + (86400 * 365), "/");
|
|
$redirect = strtok($_SERVER["REQUEST_URI"], '?');
|
|
header("Location: " . $redirect);
|
|
exit;
|
|
}
|
|
|
|
// Initialize Router
|
|
$router = new Router();
|
|
|
|
// Set base path dynamically to support subfolders (e.g. /ksh_soar)
|
|
$basePath = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
|
|
$basePath = str_replace('/public', '', $basePath);
|
|
if ($basePath === '/') {
|
|
$basePath = '';
|
|
}
|
|
$router->setBasePath($basePath);
|
|
|
|
// API / Webhook Routes
|
|
$router->mount('/api', function() use ($router) {
|
|
$router->post('/webhook/wazuh', function() {
|
|
$controller = new WazuhWebhookController();
|
|
$controller->handle();
|
|
});
|
|
|
|
$router->mount('/agent', function() use ($router) {
|
|
$router->get('/specs', function() {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->getSpecs();
|
|
});
|
|
|
|
$router->get('/quick-details', function() {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->getEndpointQuickDetails();
|
|
});
|
|
|
|
$router->get('/network', function() {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->getNetworkInspector();
|
|
});
|
|
|
|
$router->get('/processes', function() {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->getProcesses();
|
|
});
|
|
|
|
$router->get('/all-metadata', function() {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->getAllAgentsMetadata();
|
|
});
|
|
});
|
|
|
|
$router->post('/test-notification', function() {
|
|
require_once __DIR__ . '/../src/Integrations/Notifier.php';
|
|
header('Content-Type: application/json');
|
|
|
|
$type = $_POST['type'] ?? '';
|
|
$msg = "🛠️ [Test Message] This is a test notification from KSH SOAR.";
|
|
$success = false;
|
|
|
|
if ($type === 'line') {
|
|
if (empty($_ENV['LINE_NOTIFY_TOKEN'])) {
|
|
echo json_encode(['status' => 'error', 'message' => 'LINE_NOTIFY_TOKEN is not set in .env']);
|
|
return;
|
|
}
|
|
$success = \App\Integrations\Notifier::sendLineNotify($msg);
|
|
} else if ($type === 'telegram') {
|
|
if (empty($_ENV['TELEGRAM_BOT_TOKEN']) || empty($_ENV['TELEGRAM_CHAT_ID'])) {
|
|
echo json_encode(['status' => 'error', 'message' => 'Telegram tokens are not set in .env']);
|
|
return;
|
|
}
|
|
$appName = $_ENV['APP_NAME'] ?? 'KSH SOAR';
|
|
$tgMsg = "🧪 <b>[{$appName}] MOCKUP SECURITY ALERT</b>\n";
|
|
$tgMsg .= "━━━━━━━━━━━━━━━━━━━━\n";
|
|
$tgMsg .= "⚠️ <b>Risk Level:</b> <code>12</code> (High)\n";
|
|
$tgMsg .= "🆔 <b>Rule ID:</b> <code>5716</code>\n";
|
|
$tgMsg .= "🖥 <b>Agent:</b> <code>WEB-SERVER-01</code>\n";
|
|
$tgMsg .= "🌐 <b>IP Address:</b> <code>192.168.1.100</code>\n";
|
|
$tgMsg .= "━━━━━━━━━━━━━━━━━━━━\n";
|
|
$tgMsg .= "📝 <b>Description:</b>\n<i>เข้าสู่ระบบล้มเหลวหลายครั้งติดต่อกัน (อาจมีการ Brute-force)</i>\n\n";
|
|
$tgMsg .= "📍 <b>Action Taken:</b>\n";
|
|
$tgMsg .= "✅ Auto-created Investigation Case <b>#105</b>\n";
|
|
$tgMsg .= "🤖 AI Remediation Analysis queued.\n\n";
|
|
$tgMsg .= "✅ <b>System Integration Test Successful!</b>";
|
|
|
|
$success = \App\Integrations\Notifier::sendTelegram($tgMsg);
|
|
} else {
|
|
echo json_encode(['status' => 'error', 'message' => 'Invalid type']);
|
|
return;
|
|
}
|
|
|
|
if ($success) {
|
|
echo json_encode(['status' => 'success', 'message' => ucfirst($type) . ' notification sent successfully!']);
|
|
} else {
|
|
echo json_encode(['status' => 'error', 'message' => 'Failed to send notification. Please check your tokens.']);
|
|
}
|
|
});
|
|
|
|
$router->post('/clear-all-alerts', function() {
|
|
$controller = new DashboardController();
|
|
$controller->clearAllAlerts();
|
|
});
|
|
|
|
$router->post('/virustotal-check', function() {
|
|
header('Content-Type: application/json');
|
|
$type = $_POST['type'] ?? '';
|
|
$value = $_POST['value'] ?? '';
|
|
$apiKey = $_ENV['VIRUSTOTAL_API_KEY'] ?? '';
|
|
|
|
if (empty($apiKey)) {
|
|
echo json_encode(['status' => 'error', 'message' => 'VIRUSTOTAL_API_KEY is not configured in .env']);
|
|
return;
|
|
}
|
|
|
|
if (empty($value)) {
|
|
echo json_encode(['status' => 'error', 'message' => 'No value provided for scanning.']);
|
|
return;
|
|
}
|
|
|
|
$url = "";
|
|
if ($type === 'hash') {
|
|
$url = "https://www.virustotal.com/api/v3/files/" . urlencode($value);
|
|
} else if ($type === 'ip') {
|
|
$url = "https://www.virustotal.com/api/v3/ip_addresses/" . urlencode($value);
|
|
} else {
|
|
echo json_encode(['status' => 'error', 'message' => 'Invalid scan type.']);
|
|
return;
|
|
}
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
"x-apikey: {$apiKey}"
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpcode == 200) {
|
|
$data = json_decode($response, true);
|
|
$stats = $data['data']['attributes']['last_analysis_stats'] ?? null;
|
|
if ($stats) {
|
|
$malicious = $stats['malicious'] ?? 0;
|
|
$suspicious = $stats['suspicious'] ?? 0;
|
|
$total = array_sum($stats);
|
|
|
|
$html = "<div class='text-sm mt-2'>";
|
|
if ($malicious > 0) {
|
|
$html .= "<div class='bg-rose-100 text-rose-800 px-3 py-2 rounded font-bold'><i class='fas fa-biohazard mr-2'></i> Malicious: {$malicious} / {$total} engines detected this as a threat!</div>";
|
|
} else if ($suspicious > 0) {
|
|
$html .= "<div class='bg-amber-100 text-amber-800 px-3 py-2 rounded font-bold'><i class='fas fa-exclamation-triangle mr-2'></i> Suspicious: {$suspicious} / {$total} engines flagged this.</div>";
|
|
} else {
|
|
$html .= "<div class='bg-emerald-100 text-emerald-800 px-3 py-2 rounded font-bold'><i class='fas fa-shield-alt mr-2'></i> Clean: 0 / {$total} engines detected threats.</div>";
|
|
}
|
|
$vtLink = $type === 'hash' ? "https://www.virustotal.com/gui/file/{$value}" : "https://www.virustotal.com/gui/ip-address/{$value}";
|
|
$html .= "<a href='{$vtLink}' target='_blank' class='block mt-2 text-xs text-blue-600 underline hover:text-blue-800'><i class='fas fa-external-link-alt mr-1'></i> View full report on VirusTotal</a>";
|
|
$html .= "</div>";
|
|
|
|
echo json_encode(['status' => 'success', 'html' => $html]);
|
|
} else {
|
|
echo json_encode(['status' => 'error', 'message' => 'Could not parse VirusTotal analysis stats.']);
|
|
}
|
|
} else if ($httpcode == 404) {
|
|
echo json_encode(['status' => 'success', 'html' => "<div class='bg-slate-100 text-slate-600 px-3 py-2 rounded text-sm mt-2'><i class='fas fa-search mr-2'></i> No match found on VirusTotal.</div>"]);
|
|
} else {
|
|
echo json_encode(['status' => 'error', 'message' => "VirusTotal API Error (HTTP {$httpcode})"]);
|
|
}
|
|
});
|
|
|
|
$router->post('/ai-remediation', function() {
|
|
header('Content-Type: application/json');
|
|
$ruleId = $_POST['rule_id'] ?? '';
|
|
$ruleDesc = $_POST['rule_desc'] ?? '';
|
|
$level = (int)($_POST['level'] ?? 0);
|
|
|
|
try {
|
|
$db = \App\Database\Connection::getInstance();
|
|
// Create cache table automatically if it doesn't exist
|
|
$db->query("CREATE TABLE IF NOT EXISTS ai_remediation_cache (
|
|
rule_id INT PRIMARY KEY,
|
|
recommendation TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
|
|
$force = isset($_POST['force']) && $_POST['force'] == 'true';
|
|
|
|
// 1. Check cache first to save API calls
|
|
if (!$force) {
|
|
$stmt = $db->prepare("SELECT recommendation FROM ai_remediation_cache WHERE rule_id = ?");
|
|
$stmt->execute([$ruleId]);
|
|
$cachedHtml = $stmt->fetchColumn();
|
|
|
|
if ($cachedHtml) {
|
|
// Return cached response (you can add a small badge to indicate it's from cache if you want)
|
|
$cachedHtml .= "<div class='mt-2 text-[10px] text-indigo-400 text-right italic'><i class='fas fa-bolt mr-1'></i> Loaded from Cache</div>";
|
|
echo json_encode(['status' => 'success', 'html' => $cachedHtml]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 2. Setup Gemini API request
|
|
$apiKey = $_ENV['GEMINI_API_KEY'] ?? '';
|
|
if (empty($apiKey)) {
|
|
echo json_encode(['status' => 'error', 'message' => 'GEMINI_API_KEY is not configured in .env file.']);
|
|
return;
|
|
}
|
|
|
|
$prompt = "You are a senior cybersecurity SOC analyst. Analyze the following Wazuh alert and provide a concise, actionable 3-step remediation guideline.
|
|
Rule ID: {$ruleId}
|
|
Level: {$level}
|
|
Description: {$ruleDesc}
|
|
|
|
Requirements:
|
|
- Answer in Thai language.
|
|
- Output ONLY valid HTML: an unordered list <ul> with 3 <li> items. Add 'list-disc pl-4 space-y-2 mt-1' class to the <ul>.
|
|
- Wrap keywords in <strong> tags.
|
|
- Do NOT wrap the output in markdown code blocks (no ```html).";
|
|
|
|
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" . $apiKey;
|
|
$payload = [
|
|
"contents" => [
|
|
["parts" => [["text" => $prompt]]]
|
|
]
|
|
];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Bypass local cert issues
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode == 200 && $response) {
|
|
$data = json_decode($response, true);
|
|
$aiText = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
|
|
|
// Clean up possible markdown tags returned by the model
|
|
$aiText = str_replace(['```html', '```'], '', $aiText);
|
|
$html = "<div class='text-sm leading-relaxed text-indigo-900'>" . trim($aiText) . "</div>";
|
|
|
|
// 3. Save to database cache
|
|
$stmt = $db->prepare("INSERT INTO ai_remediation_cache (rule_id, recommendation) VALUES (?, ?) ON DUPLICATE KEY UPDATE recommendation = VALUES(recommendation)");
|
|
$stmt->execute([$ruleId, $html]);
|
|
|
|
echo json_encode(['status' => 'success', 'html' => $html]);
|
|
} else {
|
|
$errorMsg = 'Failed to reach Gemini API (HTTP ' . $httpCode . ')';
|
|
if ($response) {
|
|
$errData = json_decode($response, true);
|
|
if (isset($errData['error']['message'])) $errorMsg = $errData['error']['message'];
|
|
}
|
|
echo json_encode(['status' => 'error', 'message' => $errorMsg]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
|
}
|
|
});
|
|
|
|
|
|
|
|
$router->get('/dashboard/charts', function() {
|
|
$controller = new DashboardController();
|
|
$controller->getChartData();
|
|
});
|
|
|
|
$router->get('/realtime', function() {
|
|
$controller = new DashboardController();
|
|
$controller->getRealtimeUpdates();
|
|
});
|
|
});
|
|
|
|
// Web Routes
|
|
$router->get('/', function() {
|
|
$controller = new DashboardController();
|
|
$controller->index();
|
|
});
|
|
|
|
$router->get('/ai-history', function() {
|
|
$controller = new DashboardController();
|
|
$controller->aiHistory();
|
|
});
|
|
|
|
$router->get('/sysmon', function() {
|
|
$controller = new \App\Controllers\SysmonController();
|
|
$controller->index();
|
|
});
|
|
|
|
$router->post('/clear-alerts', function() {
|
|
$controller = new DashboardController();
|
|
$controller->clearOldAlerts();
|
|
});
|
|
|
|
$router->get('/search', function() {
|
|
$controller = new \App\Controllers\SearchController();
|
|
$controller->index();
|
|
});
|
|
|
|
$router->post('/api/search/ssp', function() {
|
|
$controller = new \App\Controllers\SearchController();
|
|
$controller->ssp();
|
|
});
|
|
|
|
$router->get('/settings', function() {
|
|
$controller = new DashboardController();
|
|
$controller->settings();
|
|
});
|
|
|
|
|
|
$router->get('/agents', function() {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->index();
|
|
});
|
|
|
|
$router->get('/api/agents/(\d+)/details', function($id) {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->getDetails($id);
|
|
});
|
|
|
|
$router->get('/api/agents/remote-list', function() {
|
|
$controller = new \App\Controllers\AgentController();
|
|
$controller->getRemoteList();
|
|
});
|
|
|
|
// 404 Handler
|
|
$router->set404(function() {
|
|
header('HTTP/1.1 404 Not Found');
|
|
echo "404 Not Found";
|
|
});
|
|
|
|
// Run Router
|
|
$router->run();
|