Initial commit

This commit is contained in:
Porawit Dongwang
2026-09-16 23:20:08 +07:00
commit 0041668dbb
32577 changed files with 3687927 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,161 @@
<?php
$pageTitle = "AI Knowledge Base";
ob_start();
?>
<div class="mb-6">
<h1 class="text-2xl font-bold text-slate-800"><i class="fas fa-brain text-indigo-600 mr-2"></i> AI Knowledge Base</h1>
<p class="text-slate-500 mt-1">History of AI-generated remediation guidelines. These insights are cached to accelerate future incident responses.</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div class="overflow-x-auto">
<table id="aiHistoryTable" class="w-full text-left border-collapse table-fixed">
<thead>
<tr class="bg-slate-50 text-slate-600 border-b border-slate-200 text-sm">
<th class="px-5 py-4 font-semibold w-64"><?= __('Rule Info') ?></th>
<th class="px-5 py-4 font-semibold"><?= __('AI Recommendation') ?></th>
<th class="px-5 py-4 font-semibold text-center w-40"><?= __('Cached Date') ?></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 text-sm">
<?php foreach ($histories as $h): ?>
<?php
// Extract rule groups from raw_data JSON
$groups = [];
if (isset($h['raw_data'])) {
$raw = json_decode($h['raw_data'], true);
if (isset($raw['rule']['groups'])) {
$groups = $raw['rule']['groups'];
}
}
$desc = htmlspecialchars($h['rule_desc'] ?? 'Unknown Rule Description');
?>
<tr class="hover:bg-slate-50/50 transition-colors group">
<td class="px-5 py-4">
<div class="font-mono text-xs text-indigo-600 font-bold mb-1">ID: <?= htmlspecialchars($h['rule_id']) ?></div>
<div class="text-slate-700 font-medium text-xs mb-2 leading-tight"><?= $desc ?></div>
<div class="flex flex-wrap gap-1">
<?php foreach($groups as $g): $g = trim($g); if(empty($g)) continue; ?>
<span class="bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded text-[10px] uppercase font-semibold"><?= htmlspecialchars($g) ?></span>
<?php endforeach; ?>
</div>
</td>
<td class="px-5 py-4">
<div class="bg-indigo-50/50 border border-indigo-100 rounded-lg p-3 text-indigo-900 text-sm shadow-sm relative whitespace-normal break-words w-full" id="rec-container-<?= $h['rule_id'] ?>">
<div class="overflow-hidden line-clamp-3 max-h-[4.5rem]">
<?= $h['recommendation'] ?>
</div>
<div class="mt-2 pt-2 border-t border-indigo-100/50 flex justify-between items-center">
<button onclick="regenerateAI(<?= $h['rule_id'] ?>, '<?= addslashes($desc) ?>')" class="text-slate-400 hover:text-indigo-600 text-xs font-bold uppercase transition" id="regen-btn-<?= $h['rule_id'] ?>">
<i class="fas fa-sync-alt mr-1"></i> Regenerate
</button>
<button onclick="viewAIRecommendation(<?= $h['rule_id'] ?>)" class="text-indigo-600 hover:text-indigo-800 text-xs font-bold uppercase transition">
<i class="fas fa-expand-alt mr-1"></i> Read Full Analysis
</button>
</div>
</div>
<template id="ai-rec-<?= $h['rule_id'] ?>">
<?= $h['recommendation'] ?>
</template>
</td>
<td class="px-5 py-4 text-center text-slate-500 whitespace-nowrap">
<?= date('Y-m-d H:i', strtotime($h['created_at'])) ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($histories)): ?>
<tr>
<td colspan="3" class="px-5 py-8 text-center text-slate-400">
<i class="fas fa-inbox text-3xl mb-3 block"></i>
<?= __('No AI insights cached yet.') ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- DataTables setup -->
<script>
window.regenerateAI = function(ruleId, ruleDesc) {
let btn = document.getElementById('regen-btn-' + ruleId);
let container = document.getElementById('rec-container-' + ruleId);
let tmpl = document.getElementById('ai-rec-' + ruleId);
let originalIcon = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Thinking...';
btn.disabled = true;
$.ajax({
url: '<?= $baseUrl ?>/api/ai-remediation',
method: 'POST',
data: {
rule_id: ruleId,
rule_desc: ruleDesc,
level: 0,
force: 'true'
},
success: function(res) {
btn.innerHTML = originalIcon;
btn.disabled = false;
if (res.status === 'success') {
tmpl.innerHTML = res.html;
container.querySelector('.line-clamp-3').innerHTML = res.html;
Swal.fire({
icon: 'success',
title: 'Regenerated!',
text: 'The AI insight has been updated.',
timer: 1500,
showConfirmButton: false
});
} else {
Swal.fire('Error', res.message || 'Failed to regenerate.', 'error');
}
},
error: function() {
btn.innerHTML = originalIcon;
btn.disabled = false;
Swal.fire('Error', 'Connection failed.', 'error');
}
});
};
window.viewAIRecommendation = function(ruleId) {
let template = document.getElementById('ai-rec-' + ruleId);
if (!template) return;
let rawHtml = template.innerHTML;
Swal.fire({
title: '<div class="text-indigo-800 text-lg border-b border-indigo-200 pb-2"><i class="fas fa-robot mr-2"></i> AI Investigation & Remediation</div>',
html: `<div class="text-left text-sm text-indigo-900 bg-indigo-50 p-5 rounded-lg border border-indigo-100 leading-relaxed overflow-x-auto shadow-inner mt-2">${rawHtml}</div>`,
width: '800px',
showCloseButton: true,
showConfirmButton: false,
customClass: {
container: 'font-sans'
}
});
};
$(document).ready(function() {
if($('#aiHistoryTable tbody tr td').length > 1) {
$('#aiHistoryTable').DataTable({
responsive: true,
order: [[2, 'desc']],
pageLength: 10,
language: {
search: "",
searchPlaceholder: "Search insights..."
},
dom: '<"flex justify-between items-center mb-4 px-5 pt-4"lf>rt<"flex justify-between items-center mt-4 px-5 pb-4"ip>'
});
}
});
</script>
<?php
$content = ob_get_clean();
require 'layout.php';
?>
@@ -0,0 +1,658 @@
<?php $pageTitle = __('Dashboard Overview'); ob_start(); ?>
<?php if (isset($_GET['cleared'])): ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
Swal.fire({
icon: 'success',
title: 'Action Successful',
text: 'Successfully cleared <?= (int)$_GET['cleared'] ?> old alerts.',
timer: 3000,
showConfirmButton: false,
customClass: {
popup: 'corp-card'
}
});
});
</script>
<?php endif; ?>
<div class="flex justify-end mb-6">
<?php
$baseUrl = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
$baseUrl = str_replace('/public', '', $baseUrl);
if ($baseUrl === '/') $baseUrl = '';
?>
<form id="clearForm" action="<?= $baseUrl ?>/clear-alerts" method="POST">
<button type="button" onclick="confirmClear()" class="corp-btn-danger px-4 py-2 rounded-md text-sm font-medium">
<i class="fas fa-trash-alt mr-1.5"></i> <?= __('Clear Old Alerts') ?>
</button>
</form>
</div>
<!-- Stats Section -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 mb-8">
<div class="corp-card p-5 border-t-4 border-t-slate-800 flex flex-col justify-center">
<div class="flex justify-between items-center mb-1">
<h3 class="text-slate-500 text-xs font-bold uppercase tracking-wide"><?= __('Total Alerts') ?></h3>
<i class="fas fa-chart-line text-slate-300 text-lg"></i>
</div>
<p id="stat-total" class="text-3xl font-semibold text-slate-800 transition-all duration-300"><?= number_format($stats['total_alerts']) ?></p>
</div>
<div class="corp-card p-5 border-t-4 border-t-blue-600 flex flex-col justify-center cursor-pointer hover:shadow-md transition-shadow" onclick="window.location.href='<?= $baseUrl ?>/agents'">
<div class="flex justify-between items-center mb-1">
<h3 class="text-slate-500 text-xs font-bold uppercase tracking-wide"><?= __('Total Agents') ?></h3>
<i class="fas fa-server text-blue-200 text-lg"></i>
</div>
<p id="stat-agents" class="text-3xl font-semibold text-slate-800 transition-all duration-300"><?= number_format($stats['total_agents'] ?? 0) ?></p>
</div>
<div class="corp-card p-5 border-t-4 border-t-rose-600 flex flex-col justify-center">
<div class="flex justify-between items-center mb-1">
<h3 class="text-slate-500 text-xs font-bold uppercase tracking-wide"><?= __('High Severity (12+)') ?></h3>
<i class="fas fa-exclamation-circle text-rose-200 text-lg"></i>
</div>
<p id="stat-high" class="text-3xl font-semibold text-rose-600 transition-all duration-300"><?= number_format($stats['high_alerts']) ?></p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="corp-card p-5 lg:col-span-1">
<h3 class="font-semibold text-slate-700 text-sm mb-4"><i class="fas fa-chart-pie mr-2 text-slate-400"></i> Alerts by Rule Level</h3>
<div class="relative h-64 w-full flex justify-center">
<canvas id="levelChart"></canvas>
</div>
</div>
<div class="corp-card p-5 lg:col-span-2">
<h3 class="font-semibold text-slate-700 text-sm mb-4"><i class="fas fa-chart-bar mr-2 text-slate-400"></i> Top 5 Targeted Agents</h3>
<div class="relative h-64 w-full">
<canvas id="agentChart"></canvas>
</div>
</div>
</div>
<!-- Alerts Table -->
<div class="corp-card p-0 overflow-hidden mb-6">
<div class="px-5 py-4 border-b border-slate-200 flex justify-between items-center bg-white">
<h3 class="font-semibold text-slate-800 text-base"><i class="fas fa-list-ul text-slate-400 mr-2"></i> <?= __('Recent Activity Stream') ?></h3>
<span id="liveStatus" class="text-[10px] font-bold text-emerald-600 bg-emerald-50 px-2.5 py-1 rounded border border-emerald-200 uppercase tracking-wide">
<i class="fas fa-circle mr-1 text-[8px] align-baseline animate-pulse"></i> Live
</span>
</div>
<div class="bg-blue-50/50 border-b border-blue-100 px-5 py-3 text-xs sm:text-sm flex items-start text-blue-800">
<i class="fas fa-info-circle text-blue-500 mt-0.5 mr-3 text-base"></i>
<div>
<span class="font-bold text-blue-900">หมายเหตุ:</span>
ระบบ SOAR นี้ตั้งค่ารับการแจ้งเตือนตั้งแต่ <b>Level <?= htmlspecialchars($minAlertLevel) ?> ขึ้นไป</b> เท่านั้น (แก้ไขได้ที่ไฟล์ .env ตัวแปร MIN_ALERT_LEVEL)
<br>ตารางด้านล่างจะแสดงผลแบบ Real-time เฉพาะ <b>1,000 รายการล่าสุด</b>
หากต้องการสืบค้นข้อมูลย้อนหลังทั้งหมด กรุณาไปที่เมนู <a href="<?= $baseUrl ?>/search" class="font-bold underline text-blue-700 hover:text-blue-900"><i class="fas fa-history text-[10px]"></i> Historical Search</a>
</div>
</div>
<div class="p-5">
<!-- Filter Controls -->
<div class="mb-5 flex items-center space-x-3 bg-slate-50 p-2.5 rounded border border-slate-200 inline-flex">
<label for="levelFilter" class="text-xs font-semibold text-slate-600 uppercase tracking-wide">Severity Filter:</label>
<select id="levelFilter" class="bg-white border-slate-300 text-slate-700 rounded text-sm py-1.5 px-3 outline-none focus:ring-1 focus:ring-slate-500 focus:border-slate-500 transition shadow-sm">
<option value="">All Levels</option>
<option value="Level (12|13|14|15)">High (L12+)</option>
<option value="Level (7|8|9|10|11)">Medium (L7-L11)</option>
<option value="Level [0-6]$">Low (L0-L6)</option>
</select>
</div>
<table id="alertsTable" class="w-full text-left border-collapse" style="width:100%">
<thead>
<tr class="text-slate-500 text-xs uppercase tracking-wider border-b border-slate-200 bg-slate-50">
<th class="px-4 py-3 text-center font-semibold">ID</th>
<th class="px-4 py-3 text-center font-semibold">Timestamp</th>
<th class="px-4 py-3 text-center font-semibold"><?= __('Level') ?></th>
<th class="px-4 py-3 text-center font-semibold"><?= __('Agent') ?></th>
<th class="px-4 py-3 text-left font-semibold"><?= __('Description') ?></th>
<th class="px-4 py-3 text-center font-semibold"><?= __('Action') ?></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
<?php
$maxId = 0;
foreach ($recent_alerts as $alert):
if ($alert['id'] > $maxId) $maxId = $alert['id'];
?>
<tr class="text-sm text-center hover:bg-slate-50/50 transition-colors">
<td class="px-4 py-3 text-center text-slate-500"><?= $alert['id'] ?></td>
<td class="px-4 py-3 whitespace-nowrap text-center text-slate-600 font-medium"><?= $alert['created_at'] ?></td>
<td class="px-4 py-3 text-center">
<?php
$lvl = $alert['rule_level'];
$levelClass = 'bg-slate-100 text-slate-600 border-slate-200';
if ($lvl >= 12) $levelClass = 'bg-rose-50 text-rose-600 border-rose-200';
else if ($lvl >= 8) $levelClass = 'bg-amber-50 text-amber-600 border-amber-200';
else if ($lvl >= 5) $levelClass = 'bg-blue-50 text-blue-600 border-blue-200';
?>
<span class="px-2 py-1 rounded text-xs font-bold border <?= $levelClass ?>">
Level <?= $lvl ?>
</span>
</td>
<td class="px-4 py-3 text-center">
<span class="text-slate-600 font-mono text-xs bg-slate-100 px-1.5 py-0.5 rounded border border-slate-200">
<?= htmlspecialchars($alert['agent_name']) ?>
</span>
</td>
<td class="px-4 py-3 max-w-sm text-left whitespace-normal break-words">
<span class="text-slate-700 font-medium"><?= htmlspecialchars($alert['description']) ?></span>
<div class="text-[11px] text-slate-400 mt-0.5"><i class="fas fa-hashtag text-slate-300"></i> Rule ID: <?= $alert['rule_id'] ?></div>
</td>
<td class="px-4 py-3 text-center">
<button onclick='viewDetails(this)' data-raw='<?= htmlspecialchars($alert['raw_data'], ENT_QUOTES, 'UTF-8') ?>' class="corp-btn-secondary px-3 py-1.5 rounded text-xs">
<i class="fas fa-search mr-1"></i> View
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let lastAlertId = <?= count($recent_alerts) > 0 ? $recent_alerts[0]['id'] : 0 ?>;
let alertsTable;
let levelChart = null;
let agentChart = null;
function loadCharts() {
$.ajax({
url: '<?= $baseUrl ?>/api/dashboard/charts',
method: 'GET',
success: function(res) {
if (levelChart) levelChart.destroy();
if (agentChart) agentChart.destroy();
levelChart = new Chart(document.getElementById('levelChart'), {
type: 'doughnut',
data: {
labels: res.levels.labels,
datasets: [{
data: res.levels.data,
backgroundColor: ['#3b82f6', '#f59e0b', '#ef4444', '#10b981', '#8b5cf6', '#64748b']
}]
},
options: { maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { boxWidth: 12 } } } }
});
agentChart = new Chart(document.getElementById('agentChart'), {
type: 'bar',
data: {
labels: res.agents.labels,
datasets: [{
label: 'Alerts',
data: res.agents.data,
backgroundColor: '#3b82f6',
borderRadius: 4
}]
},
options: { maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } }
});
}
});
}
document.addEventListener("DOMContentLoaded", function() {
loadCharts();
setInterval(loadCharts, 60000); // Reload charts every 60s
alertsTable = $('#alertsTable').DataTable({
responsive: true,
order: [[0, 'desc']],
language: {
search: "",
searchPlaceholder: "Search alerts..."
},
pageLength: 10,
dom: '<"flex justify-between items-center mb-4 px-5 pt-4"lf>rt<"flex justify-between items-center mt-4 px-5 pb-4"ip>'
});
// Filter Logic
$('#levelFilter').on('change', function() {
let val = $(this).val();
if (val) {
alertsTable.column(2).search(val, true, false).draw();
} else {
alertsTable.column(2).search('', true, false).draw();
}
});
// Start realtime updates polling
setInterval(pollNewAlerts, 10000);
});
function pollNewAlerts() {
let liveStatus = document.getElementById('liveStatus');
if(liveStatus) liveStatus.innerHTML = '<i class="fas fa-sync fa-spin mr-1"></i> Syncing...';
$.ajax({
url: '<?= $baseUrl ?>/api/realtime?last_alert_id=' + lastAlertId,
method: 'GET',
success: function(res) {
if (res.status === 'success') {
$('#stat-total-alerts').text(res.stats.total_alerts.toLocaleString());
$('#stat-open-cases').text(res.stats.open_cases.toLocaleString());
$('#stat-high-alerts').text(res.stats.high_alerts.toLocaleString());
if (res.new_alerts && res.new_alerts.length > 0) {
res.new_alerts.forEach(function(alert) {
let levelClass = 'bg-slate-100 text-slate-600 border-slate-200';
if (alert.rule_level >= 12) levelClass = 'bg-rose-50 text-rose-600 border-rose-200';
else if (alert.rule_level >= 8) levelClass = 'bg-amber-50 text-amber-600 border-amber-200';
else if (alert.rule_level >= 5) levelClass = 'bg-blue-50 text-blue-600 border-blue-200';
let levelHtml = `<span class="px-2 py-1 rounded text-xs font-bold border ${levelClass}">Level ${alert.rule_level}</span>`;
let descHtml = `<span class="text-slate-700 font-medium">${alert.description}</span>
<div class="text-[11px] text-slate-400 mt-0.5"><i class="fas fa-hashtag text-slate-300"></i> Rule ID: ${alert.rule_id}</div>`;
let agentHtml = `<span class="text-slate-600 font-mono text-xs bg-slate-100 px-1.5 py-0.5 rounded border border-slate-200">${alert.agent_name}</span>`;
let rawSafe = "";
try { rawSafe = alert.raw_data.replace(/'/g, "&#39;"); } catch(e) {}
let btnHtml = `<button onclick="viewDetails(this)" data-raw='${rawSafe}' class="corp-btn-secondary px-3 py-1.5 rounded text-xs"><i class="fas fa-search mr-1"></i> View</button>`;
let newRow = alertsTable.row.add([
alert.id,
alert.created_at,
levelHtml,
agentHtml,
descHtml,
btnHtml
]).draw(false).node();
$(newRow).addClass('text-sm text-center hover:bg-slate-50/50 transition-colors bg-orange-50 duration-1000');
$(newRow).find('td').eq(0).addClass('px-4 py-3 text-center text-slate-500');
$(newRow).find('td').eq(1).addClass('px-4 py-3 whitespace-nowrap text-center text-slate-600 font-medium');
$(newRow).find('td').eq(2).addClass('px-4 py-3 text-center');
$(newRow).find('td').eq(3).addClass('px-4 py-3 text-center');
$(newRow).find('td').eq(4).addClass('px-4 py-3 max-w-sm text-left whitespace-normal break-words');
$(newRow).find('td').eq(5).addClass('px-4 py-3 text-center');
setTimeout(() => { $(newRow).removeClass('bg-orange-50'); }, 3000);
if (parseInt(alert.id) > lastAlertId) {
lastAlertId = parseInt(alert.id);
}
});
Toastify({
text: `Received ${res.new_alerts.length} new alerts`,
duration: 3000,
close: true,
gravity: "top",
position: "right",
style: {
background: "#fff",
color: "#334155",
borderLeft: "4px solid #f97316",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
borderRadius: "4px"
}
}).showToast();
}
}
if(liveStatus) setTimeout(() => {
liveStatus.innerHTML = '<i class="fas fa-circle text-emerald-500 mr-1 text-[8px] animate-pulse"></i> Live';
}, 500);
},
error: function() {
if(liveStatus) liveStatus.innerHTML = '<i class="fas fa-exclamation-triangle text-rose-500 mr-1"></i> Offline';
}
});
}
window.scanVirusTotal = function(type, value, btnElement) {
let originalHtml = btnElement.innerHTML;
btnElement.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Scanning...';
btnElement.disabled = true;
let container = btnElement.closest('.bg-slate-50').querySelector('#vt-result-container');
$.ajax({
url: '<?= $baseUrl ?>/api/virustotal-check',
method: 'POST',
data: { type: type, value: value },
success: function(res) {
if (res.status === 'success') {
container.innerHTML = res.html;
btnElement.innerHTML = '<i class="fas fa-check mr-1"></i> Scanned';
} else {
Swal.fire('Error', res.message, 'error');
btnElement.innerHTML = originalHtml;
btnElement.disabled = false;
}
},
error: function() {
Swal.fire('Error', 'Failed to connect to VT API.', 'error');
btnElement.innerHTML = originalHtml;
btnElement.disabled = false;
}
});
};
window.viewDetails = function(btnElement) {
try {
let rawDataStr = btnElement.getAttribute('data-raw');
if (rawDataStr.startsWith('"') && rawDataStr.endsWith('"')) {
rawDataStr = JSON.parse(rawDataStr);
}
let data = typeof rawDataStr === 'string' ? JSON.parse(rawDataStr) : rawDataStr;
let ruleId = data.rule ? data.rule.id : 'N/A';
let ruleDesc = data.rule ? data.rule.description : 'No Description';
let fullLog = data.full_log || 'N/A';
let agentIp = data.agent ? data.agent.ip : 'N/A';
if (agentIp !== 'N/A' && agentIp.includes(',')) {
let ips = agentIp.split(',').map(i => i.trim()).filter(i => !i.includes(':'));
if (ips.length > 0) agentIp = ips.join(', ');
} else if (agentIp !== 'N/A' && agentIp.includes(':')) {
// Ignore if it's solely an IPv6 address, but maybe we just leave it if there's no comma.
// Or if we strictly want IPv4 only:
agentIp = 'N/A (IPv6)';
}
let extractThreatTarget = (data) => {
if (data.syscheck && data.syscheck.sha256_after) return { target: data.syscheck.sha256_after, type: 'hash' };
if (data.syscheck && data.syscheck.md5_after) return { target: data.syscheck.md5_after, type: 'hash' };
if (data.syscheck && data.syscheck.sha1_after) return { target: data.syscheck.sha1_after, type: 'hash' };
if (data.data && data.data.srcip && data.data.srcip !== '127.0.0.1') return { target: data.data.srcip, type: 'ip' };
if (data.data && data.data.destip && data.data.destip !== '127.0.0.1') return { target: data.data.destip, type: 'ip' };
return { target: null, type: null };
};
let threat = extractThreatTarget(data);
let vtHtml = '';
if (threat.target) {
let typeLabel = threat.type === 'hash' ? 'File Hash' : 'IP Address';
vtHtml = `
<div class="bg-slate-50 border border-slate-200 p-4 mt-4 rounded shadow-sm">
<strong class="text-slate-800 block mb-3 text-sm uppercase tracking-wide border-b border-slate-200 pb-2">
<i class="fas fa-shield-virus text-slate-400 mr-2"></i> Threat Intelligence (VirusTotal)
</strong>
<div class="flex items-center justify-between">
<div>
<span class="text-slate-500 text-xs font-bold uppercase block">${typeLabel}</span>
<span class="font-mono text-sm font-semibold text-slate-800 break-all">${threat.target}</span>
</div>
<button onclick="scanVirusTotal('${threat.type}', '${threat.target}', this)" class="bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1.5 rounded text-xs font-bold uppercase shadow transition shrink-0 ml-4">
<i class="fas fa-search mr-1"></i> Scan Now
</button>
</div>
<div id="vt-result-container"></div>
</div>`;
}
let extraDetailsHtml = '';
if (data.data && data.data.win && data.data.win.system) {
let win = data.data.win.system;
let evData = data.data.win.eventdata || {};
let paramsHtml = '';
Object.keys(evData).forEach(key => {
paramsHtml += `<tr><td class="py-1.5 px-3 border-b border-slate-100 text-slate-500 font-semibold w-1/3">${key}</td><td class="py-1.5 px-3 border-b border-slate-100 text-slate-800 break-words">${evData[key]}</td></tr>`;
});
extraDetailsHtml = `
<div class="bg-blue-50 border border-blue-100 p-4 rounded mt-4">
<strong class="text-blue-800 block mb-2 text-sm"><i class="fab fa-windows mr-2"></i> Windows Event Details</strong>
<div class="grid grid-cols-2 gap-2 text-xs mb-3">
<div><span class="text-slate-500 uppercase font-bold">Provider:</span> <span class="text-slate-800 font-medium">${win.providerName || 'N/A'}</span></div>
<div><span class="text-slate-500 uppercase font-bold">Event ID:</span> <span class="text-slate-800 font-medium">${win.eventID || 'N/A'}</span></div>
<div><span class="text-slate-500 uppercase font-bold">Channel:</span> <span class="text-slate-800 font-medium">${win.channel || 'N/A'}</span></div>
</div>
${paramsHtml ? `<table class="w-full text-left text-xs bg-white border border-slate-200 rounded overflow-hidden"><tbody>${paramsHtml}</tbody></table>` : ''}
</div>`;
}
let getRemediationHtml = (ruleId, desc, level) => {
return `
<div id="ai-remediation-container" class="bg-indigo-50 border border-indigo-100 p-4 mt-4 rounded shadow-sm relative overflow-hidden">
<div class="absolute inset-0 bg-indigo-100/50 animate-pulse" id="ai-loading-bg"></div>
<strong class="text-indigo-800 block mb-3 relative z-10 flex items-center border-b border-indigo-200 pb-2">
<i class="fas fa-robot mr-2 text-lg"></i> AI Investigation & Remediation
</strong>
<div id="ai-remediation-content" class="relative z-10 text-indigo-900 min-h-[40px] flex flex-col justify-center">
<div class="flex items-center text-sm font-medium text-indigo-600">
<i class="fas fa-circle-notch fa-spin text-indigo-500 mr-2 text-base"></i> Analyzing event ${ruleId} and generating recommendations...
</div>
</div>
</div>`;
};
let htmlContent = `
<div class="text-left">
<div class="mb-4">
<span class="text-xs font-bold px-2 py-1 bg-slate-100 text-slate-500 rounded border border-slate-200 mr-2">Rule: ${ruleId}</span>
<span class="text-xs font-bold px-2 py-1 ${data.rule && data.rule.level >= 12 ? 'bg-rose-100 text-rose-600 border-rose-200' : 'bg-blue-100 text-blue-600 border-blue-200'} rounded border">Level: ${data.rule ? data.rule.level : 'N/A'}</span>
</div>
<div class="bg-slate-800 text-white p-4 rounded-md mb-4 shadow-inner">
<p class="text-sm font-medium leading-relaxed">${ruleDesc}</p>
</div>
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<strong class="text-slate-800 block mb-3 text-sm uppercase tracking-wide border-b border-slate-200 pb-2"><i class="fas fa-server text-slate-400 mr-2"></i> Endpoint Details</strong>
<div class="grid grid-cols-2 gap-y-3 gap-x-4 mt-2">
<div><span class="text-slate-500 text-xs uppercase font-bold block mb-0.5">Hostname</span> <span class="font-medium text-slate-800">${data.agent.name}</span></div>
<div><span class="text-slate-500 text-xs uppercase font-bold block mb-0.5">IP Address</span> <span class="font-mono text-slate-800">${agentIp}</span></div>
<div><span class="text-slate-500 text-xs uppercase font-bold block mb-0.5">Agent ID</span> <span class="font-mono text-slate-800">${data.agent.id || 'N/A'}</span></div>
</div>
<div class="grid grid-cols-2 gap-y-3 gap-x-4 mt-3 pt-3 border-t border-slate-100">
<div>
<span class="text-slate-500 text-[10px] uppercase font-bold block mb-1"><i class="fas fa-barcode text-slate-400 mr-1"></i> Asset ID (เลขครุภัณฑ์)</span>
${data.agent && data.agent.labels && data.agent.labels.asset_id ?
`<span class="font-mono text-xs font-bold text-indigo-700 bg-indigo-50 px-2 py-1 rounded border border-indigo-200">${data.agent.labels.asset_id}</span>` :
`<span class="text-slate-400 text-xs italic">Not found</span>`}
</div>
<div>
<span class="text-slate-500 text-[10px] uppercase font-bold block mb-1"><i class="fas fa-map-marker-alt text-slate-400 mr-1"></i> Location (จุดติดตั้ง)</span>
${data.agent && data.agent.labels && data.agent.labels.location ?
`<span class="font-medium text-xs text-slate-700 bg-slate-100 px-2 py-1 rounded border border-slate-200">${data.agent.labels.location}</span>` :
`<span class="text-slate-400 text-xs italic">Not found</span>`}
</div>
</div>
<div id="endpoint-specs-container" class="mt-4 pt-3 border-t border-slate-200 hidden">
<span class="text-slate-500 text-xs uppercase font-bold block mb-2"><i class="fas fa-microchip mr-1"></i> Hardware Specifications</span>
<div id="endpoint-specs-content" class="text-xs text-slate-600 grid grid-cols-2 gap-2">
<span class="text-slate-400 italic"><i class="fas fa-circle-notch fa-spin mr-1"></i> Loading specs...</span>
</div>
</div>
</div>
${extraDetailsHtml}
${fullLog !== 'N/A' && !extraDetailsHtml ? `
<div class="bg-slate-50 p-4 rounded border border-slate-200 mt-4">
<strong class="text-slate-800 block mb-3 text-sm uppercase tracking-wide border-b border-slate-200 pb-2"><i class="fas fa-file-alt text-slate-400 mr-2"></i> Original Raw Log</strong>
<div class="text-xs bg-white p-3 border border-slate-200 rounded font-mono text-slate-600 break-words overflow-x-auto">
${fullLog.replace(/</g, '&lt;').replace(/>/g, '&gt;')}
</div>
</div>
` : ''}
${vtHtml}\n ${getRemediationHtml(ruleId, ruleDesc, data.rule ? data.rule.level : 0)}
<details class="group bg-slate-50 border border-slate-200 rounded mt-4">
<summary class="cursor-pointer font-bold text-sm text-slate-700 uppercase tracking-wide p-3 select-none flex items-center justify-between">
<span><i class="fas fa-code text-slate-400 mr-2"></i> Developer Raw JSON</span>
<i class="fas fa-chevron-down text-slate-400 transition-transform group-open:rotate-180"></i>
</summary>
<div class="p-3 border-t border-slate-200">
<pre class="bg-slate-900 text-slate-300 p-3 rounded font-mono text-[10px] overflow-auto max-h-64 whitespace-pre-wrap">${JSON.stringify(data, null, 2)}</pre>
</div>
</details>
</div>
`;
Swal.fire({
title: '<span class="text-slate-800 font-bold text-lg border-b-2 border-blue-500 pb-1">Deep Event Inspection</span>',
html: htmlContent,
icon: '',
confirmButtonText: 'Close Window',
buttonsStyling: false,
customClass: {
popup: 'corp-card rounded-md',
confirmButton: 'corp-btn-primary px-6 py-2.5 rounded text-sm font-medium mt-4 w-full'
},
width: '750px'
});
if (data.agent && data.agent.id) {
document.getElementById('endpoint-specs-container').classList.remove('hidden');
$.ajax({
url: '<?= $baseUrl ?>/api/agent/quick-details?agent_id=' + data.agent.id,
method: 'GET',
success: function(res) {
let content = document.getElementById('endpoint-specs-content');
let html = '';
if (res.hardware) {
let hw = res.hardware;
let cpuName = hw.cpu_name || (hw.cpu && hw.cpu.name) || 'N/A';
let ramTotal = hw.ram_total || (hw.ram && hw.ram.total) || 0;
let ramFree = hw.ram_free || (hw.ram && hw.ram.free) || 0;
let ramTotalGB = ramTotal ? (ramTotal / 1024 / 1024).toFixed(1) + ' GB' : 'N/A';
let ramUsedHTML = 'N/A';
if (ramTotal > 0 && ramFree > 0) {
let ramUsed = ramTotal - ramFree;
let ramUsedPercent = Math.round((ramUsed / ramTotal) * 100);
let color = ramUsedPercent > 85 ? 'text-rose-600' : (ramUsedPercent > 70 ? 'text-amber-500' : 'text-emerald-600');
ramUsedHTML = `<span class="font-medium text-slate-800">${ramTotalGB}</span> (<span class="${color} font-bold">${ramUsedPercent}% Used</span>)`;
} else {
ramUsedHTML = `<span class="font-medium text-slate-800">${ramTotalGB}</span>`;
}
html += `
<div><span class="text-slate-400 font-semibold">CPU:</span> <span class="font-medium text-slate-800" title="${cpuName}">${cpuName.length > 25 ? cpuName.substring(0, 25) + '...' : cpuName}</span></div>
<div><span class="text-slate-400 font-semibold">RAM:</span> ${ramUsedHTML}</div>
`;
} else if (res.error) {
html += `<div class="col-span-2 text-rose-500 text-[10px] break-words">HW Error: ${res.error}</div>`;
}
if (res.indexer_status && res.indexer_status !== 'OK') {
html += `<div class="col-span-2 mt-2 bg-amber-50 border border-amber-200 text-amber-700 p-2 rounded text-[10px]">
<i class="fas fa-exclamation-triangle mr-1"></i> <b>Database Warning:</b> Cannot connect to Wazuh Indexer (Status: ${res.indexer_status}). Please check .env credentials. Disk and Remote info will be unavailable.
</div>`;
}
if (res.model_info && res.model_info.PCModel) {
html += `<div><span class="text-slate-400 font-semibold">Model:</span> <span class="font-medium text-slate-800 truncate" title="${res.model_info.PCModel}">${res.model_info.PCModel}</span></div>`;
}
if (res.manufacturer_info && res.manufacturer_info.PCManufacturer) {
html += `<div><span class="text-slate-400 font-semibold">Brand:</span> <span class="font-medium text-slate-800">${res.manufacturer_info.PCManufacturer}</span></div>`;
}
if (res.disk_info) {
let disks = Array.isArray(res.disk_info) ? res.disk_info : [res.disk_info];
for (let d of disks) {
let dLetter = d.DriveLetter || d.Drive || d.DeviceID || d.Name || '';
if (dLetter.toUpperCase().includes('C')) {
let total = parseFloat(d.Total_GB || d.SizeGB || (d.Size ? (d.Size/1073741824).toFixed(2) : 0));
let free = parseFloat(d.Free_GB || d.FreeGB || (d.FreeSpace ? (d.FreeSpace/1073741824).toFixed(2) : 0));
if (total > 0) {
let used = total - free;
let usedPercent = Math.round((used / total) * 100);
let freePercent = Math.round((free / total) * 100);
let color = freePercent < 15 ? 'text-rose-600' : 'text-emerald-600';
html += `<div class="col-span-2 mt-1 bg-slate-100 p-2 rounded border border-slate-200">
<div class="flex justify-between mb-1">
<span class="text-slate-500 font-bold text-[11px]"><i class="fas fa-hdd mr-1"></i> Drive C: Space</span>
<span class="${color} font-bold text-[11px]">${freePercent}% Free</span>
</div>
<div class="w-full bg-slate-300 rounded-full h-1.5 mb-1 overflow-hidden">
<div class="bg-blue-500 h-1.5 rounded-full ${usedPercent > 85 ? '!bg-rose-500' : ''}" style="width: ${usedPercent}%"></div>
</div>
<div class="flex justify-between text-[10px] text-slate-500">
<span>Used: ${used.toFixed(1)} GB (${usedPercent}%)</span>
<span>Total: ${total.toFixed(1)} GB</span>
</div>
</div>`;
}
break;
}
}
}
let remoteHtml = [];
if (res.tv_info && res.tv_info.TeamViewerID && res.tv_info.TeamViewerID !== 'Not Found') {
remoteHtml.push(`<span class="inline-flex items-center text-[11px]"><img src="https://www.teamviewer.com/favicon.ico" class="w-3 h-3 mr-1 opacity-80 rounded-sm"> <span class="font-medium text-slate-700">${res.tv_info.TeamViewerID}</span></span>`);
}
if (res.anydesk_info && res.anydesk_info.AnyDeskID && res.anydesk_info.AnyDeskID !== 'Not Found') {
remoteHtml.push(`<span class="inline-flex items-center text-[11px] ml-3"><img src="https://anydesk.com/favicon.ico" class="w-3 h-3 mr-1 opacity-80 rounded-sm"> <span class="font-medium text-slate-700">${res.anydesk_info.AnyDeskID}</span></span>`);
}
if (remoteHtml.length > 0) {
html += `<div class="col-span-2 mt-1"><span class="text-slate-400 font-semibold mr-1">Remote ID:</span> ${remoteHtml.join('')}</div>`;
}
if (res.netiface && res.netiface.length > 0) {
let activeIface = null;
for (let net of res.netiface) {
let name = (net.name || '').toLowerCase();
if (net.state === 'up' && net.mac && net.mac !== '00:00:00:00:00:00' && !name.includes('loopback') && !name.includes('pseudo')) {
activeIface = net;
if (name.includes('wi-fi') || name.includes('wlan') || name.includes('wireless')) break;
}
}
if (activeIface) {
let nameLower = activeIface.name.toLowerCase();
let isWifi = nameLower.includes('wi-fi') || nameLower.includes('wlan') || nameLower.includes('wireless');
let netType = isWifi ? 'WiFi' : 'LAN';
let icon = isWifi ? 'fa-wifi text-blue-500' : 'fa-ethernet text-slate-500';
html += `<div class="col-span-2 mt-1"><span class="text-slate-400 font-semibold">Network:</span> <span class="font-medium text-slate-800"><i class="fas ${icon} mr-1"></i> ${netType} <span class="text-slate-400 text-[10px] font-normal">(${activeIface.name})</span></span></div>`;
}
}
content.innerHTML = html || '<span class="text-slate-400 col-span-2">No hardware or custom inventory specs available.</span>';
}
});
$.ajax({
url: '<?= $baseUrl ?>/api/ai-remediation',
method: 'POST',
data: {
rule_id: ruleId,
rule_desc: ruleDesc,
level: data.rule ? data.rule.level : 0
},
success: function(res) {
$('#ai-loading-bg').remove();
if(res.status === 'success') {
$('#ai-remediation-content').html(res.html).hide().fadeIn(400);
} else {
let errorMsg = res.message || 'Failed to generate AI recommendations.';
$('#ai-remediation-content').html('<span class="text-rose-600"><i class="fas fa-exclamation-triangle mr-1"></i> ' + errorMsg + '</span>');
}
},
error: function() {
$('#ai-loading-bg').remove();
$('#ai-remediation-content').html('<span class="text-rose-600"><i class="fas fa-exclamation-triangle mr-1"></i> Error connecting to AI engine.</span>');
}
});
}
} catch(e) {
console.error(e);
Swal.fire('Error', 'Could not parse raw data.', 'error');
}
};
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/layout.php';
?>
@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="en">
<head>
<?php
$baseUrl = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
$baseUrl = str_replace('/public', '', $baseUrl);
if ($baseUrl === '/') $baseUrl = '';
$appName = $_ENV['APP_NAME'] ?? 'KSH SOAR';
?>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($appName) ?> - Wazuh Integration</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="<?= $baseUrl ?>/logo.png">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<!-- jQuery (Required for DataTables) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- DataTables with Tailwind CSS Integration -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.tailwindcss.css">
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.tailwindcss.js"></script>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Toastify -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<style>
/* Corporate Enterprise UI Styles */
body {
background-color: #f8fafc; /* slate-50 */
}
.corp-card {
background: #ffffff;
border: 1px solid #e2e8f0; /* slate-200 */
box-shadow: 0 1px 3px 0 rgba(15, 23, 42, 0.05), 0 1px 2px -1px rgba(15, 23, 42, 0.05);
border-radius: 0.5rem;
transition: box-shadow 0.2s ease-in-out;
}
.corp-card:hover {
box-shadow: 0 4px 6px -1px rgba(15, 23, 42, 0.08), 0 2px 4px -2px rgba(15, 23, 42, 0.04);
}
.corp-btn-primary {
background-color: #0f172a; /* slate-900 */
color: #ffffff;
border: 1px solid #0f172a;
transition: all 0.2s;
}
.corp-btn-primary:hover {
background-color: #1e293b; /* slate-800 */
box-shadow: 0 4px 6px -1px rgba(15, 23, 42, 0.2);
}
.corp-btn-danger {
background-color: #ef4444; /* red-500 */
color: #ffffff;
border: 1px solid #ef4444;
transition: all 0.2s;
}
.corp-btn-danger:hover {
background-color: #dc2626; /* red-600 */
box-shadow: 0 4px 6px -1px rgba(239, 68, 68, 0.3);
}
.sidebar-link {
transition: all 0.2s;
border-left: 4px solid transparent;
}
.sidebar-link:hover {
background-color: rgba(255,255,255,0.05);
color: #ffffff;
border-left: 4px solid rgba(255,255,255,0.2);
}
.sidebar-link.active {
background-color: rgba(59, 130, 246, 0.15); /* light blue bg */
color: #ffffff;
border-left: 4px solid #3b82f6; /* blue-500 border */
}
/* DataTables Custom Overrides */
div.dt-container { padding-top: 0.5rem; }
div.dt-search input {
border: 1px solid #cbd5e1;
border-radius: 0.375rem;
padding: 0.35rem 0.75rem;
outline: none;
font-size: 0.875rem;
}
div.dt-search input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
div.dt-length select {
border: 1px solid #cbd5e1;
border-radius: 0.375rem;
padding: 0.25rem 1.5rem 0.25rem 0.5rem;
font-size: 0.875rem;
}
</style>
</head>
<body class="flex h-screen overflow-hidden font-sans text-slate-800 antialiased">
<!-- Sidebar Overlay (Mobile) -->
<div id="sidebarOverlay" class="fixed inset-0 bg-slate-900/60 z-20 hidden lg:hidden transition-opacity" onclick="toggleSidebar()"></div>
<!-- Sidebar -->
<aside id="sidebar" class="w-64 bg-slate-900 h-full flex flex-col fixed lg:relative z-30 transition-transform duration-300 -translate-x-full lg:translate-x-0 border-r border-slate-800">
<div class="p-5 flex justify-between items-center border-b border-slate-800">
<div class="flex items-center">
<img src="<?= $baseUrl ?>/logo.png" alt="Logo" class="w-10 h-10 object-contain mr-3 bg-white rounded p-0.5">
<h1 class="text-lg font-bold text-white tracking-wide truncate" title="<?= htmlspecialchars($appName) ?>"><?= htmlspecialchars($appName) ?></h1>
</div>
<button class="lg:hidden text-slate-400 hover:text-white transition" onclick="toggleSidebar()">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider mt-2">
<?= __('MAIN NAVIGATION') ?>
</div>
<?php
$currentUri = $_SERVER['REQUEST_URI'];
// Normalize URI to ignore query params
$pathOnly = parse_url($currentUri, PHP_URL_PATH);
// If it's a sub-directory installation, remove base url
$relPath = str_replace($baseUrl, '', $pathOnly);
if ($relPath === '') $relPath = '/';
?>
<nav class="flex-1 px-3 space-y-1 overflow-y-auto pb-4 text-sm font-medium">
<a href="<?= $baseUrl ?>/" class="sidebar-link block px-3 py-2.5 rounded-r-md text-slate-300 <?= ($relPath === '/' || $relPath === '/index.php') ? 'active' : '' ?>"><i class="fas fa-tachometer-alt mr-3 w-5 text-center"></i> <?= __('Dashboard') ?></a>
<a href="<?= $baseUrl ?>/agents" class="sidebar-link block px-3 py-2.5 rounded-r-md text-slate-300 <?= (strpos($relPath, '/agents') === 0) ? 'active' : '' ?>"><i class="fas fa-server mr-3 w-5 text-center"></i> <?= __('Agents (Devices)') ?></a>
<a href="<?= $baseUrl ?>/sysmon" class="sidebar-link block px-3 py-2.5 rounded-r-md text-slate-300 <?= (strpos($relPath, '/sysmon') === 0) ? 'active' : '' ?>"><i class="fas fa-desktop mr-3 w-5 text-center text-teal-400"></i> <?= __('Sysmon / EDR') ?></a>
<a href="<?= $baseUrl ?>/search" class="sidebar-link block px-3 py-2.5 rounded-r-md text-slate-300 <?= (strpos($relPath, '/search') === 0) ? 'active' : '' ?>"><i class="fas fa-history mr-3 w-5 text-center"></i> <?= __('Historical Search') ?></a>
<a href="<?= $baseUrl ?>/ai-history" class="sidebar-link block px-3 py-2.5 rounded-r-md text-slate-300 <?= (strpos($relPath, '/ai-history') === 0) ? 'active' : '' ?>"><i class="fas fa-brain mr-3 w-5 text-center text-indigo-400"></i> <?= __('AI Knowledge Base') ?></a>
<div class="px-3 py-2">
<div class="h-px w-full bg-gradient-to-r from-transparent via-slate-700 to-transparent my-1"></div>
</div>
<a href="<?= $baseUrl ?>/settings" class="sidebar-link block px-3 py-2.5 rounded-r-md text-slate-300 <?= (strpos($relPath, '/settings') === 0) ? 'active' : '' ?>"><i class="fas fa-cog mr-3 w-5 text-center"></i> <?= __('Settings') ?></a>
</nav>
</aside>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-full overflow-hidden w-full relative">
<!-- Top Header -->
<header class="bg-white px-6 py-4 flex justify-between items-center z-10 border-b border-slate-200 sticky top-0 shadow-sm">
<div class="flex items-center">
<button class="lg:hidden text-slate-500 hover:text-slate-800 mr-4 focus:outline-none transition" onclick="toggleSidebar()">
<i class="fas fa-bars text-xl"></i>
</button>
<h2 class="text-lg font-semibold text-slate-800"><?= $pageTitle ?? 'Dashboard' ?></h2>
</div>
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-2 text-xs font-semibold mr-2 border border-slate-200 rounded p-0.5 bg-slate-50">
<a href="?lang=en" class="px-2 py-1 rounded <?= ($_COOKIE['lang'] ?? 'en') === 'en' ? 'bg-slate-200 text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700' ?>">EN</a>
<a href="?lang=th" class="px-2 py-1 rounded <?= ($_COOKIE['lang'] ?? 'en') === 'th' ? 'bg-slate-200 text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700' ?>">TH</a>
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800 border border-emerald-200 hidden sm:inline-flex">
<span class="w-2 h-2 mr-1.5 bg-emerald-500 rounded-full animate-pulse"></span>
<?= __('System Active') ?>
</span>
</div>
</header>
<!-- Content Area -->
<div class="p-6 lg:p-8 flex-1 overflow-y-auto pb-24">
<?= $content ?>
</div>
<!-- Fixed Bottom Footer -->
<footer class="bg-white border-t border-slate-200 p-4 text-center absolute bottom-0 w-full z-10 shadow-[0_-1px_2px_rgba(0,0,0,0.02)]">
<p class="text-xs text-slate-500">
&copy; <?= date('Y') ?> <?= htmlspecialchars($_ENV['FOOTER_TEXT'] ?? ($appName . ' Security Management System. Powered by Wazuh. All rights reserved.')) ?>
</p>
</footer>
</main>
<script>
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
sidebar.classList.toggle('-translate-x-full');
overlay.classList.toggle('hidden');
}
</script>
</body>
</html>
@@ -0,0 +1,492 @@
<?php ob_start(); ?>
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Historical Search</h2>
<p class="text-slate-500 text-sm mt-1">Search through all historical alerts using Server-Side Processing (MySQL). Optimized for millions of rows.</p>
</div>
</div>
<div class="corp-card p-0 overflow-hidden">
<div class="bg-slate-50 border-b border-slate-200 p-4 font-semibold text-slate-700 flex justify-between items-center flex-wrap gap-4">
<span class="flex items-center"><i class="fas fa-database text-slate-400 mr-2"></i> All Alerts Database</span>
<div class="flex items-center space-x-2 text-sm font-normal">
<div class="flex items-center space-x-1">
<span class="text-slate-500">From:</span>
<input type="date" id="startDate" class="border border-slate-300 rounded px-2 py-1 text-slate-700 focus:outline-none focus:border-blue-500">
</div>
<div class="flex items-center space-x-1">
<span class="text-slate-500">To:</span>
<input type="date" id="endDate" class="border border-slate-300 rounded px-2 py-1 text-slate-700 focus:outline-none focus:border-blue-500">
</div>
<button id="btnFilter" class="corp-btn-primary px-3 py-1 rounded text-xs"><i class="fas fa-filter mr-1"></i> Filter</button>
<button id="btnClearFilter" class="corp-btn-secondary px-3 py-1 rounded text-xs"><i class="fas fa-times"></i></button>
</div>
</div>
<div class="p-4">
<table id="searchTable" class="w-full text-sm text-left">
<thead class="text-xs text-slate-500 uppercase bg-slate-50 border-y border-slate-200">
<tr>
<th class="px-4 py-3 font-semibold"><?= __('Time') ?></th>
<th class="px-4 py-3 font-semibold"><?= __('Level') ?></th>
<th class="px-4 py-3 font-semibold w-1/2"><?= __('Description') ?></th>
<th class="px-4 py-3 font-semibold"><?= __('Agent') ?></th>
<th class="px-4 py-3 font-semibold text-center"><?= __('Action') ?></th>
</tr>
</thead>
<tbody>
<!-- Data populated via AJAX SSP -->
</tbody>
</table>
</div>
</div>
<script>
$(document).ready(function() {
let table = $('#searchTable').DataTable({
"processing": true,
"serverSide": true,
"ajax": {
"url": "<?= $baseUrl ?>/api/search/ssp",
"type": "POST",
"data": function(d) {
d.startDate = $('#startDate').val();
d.endDate = $('#endDate').val();
},
"error": function(xhr, error, thrown) {
console.error("DataTables SSP Error:", error, thrown);
Swal.fire('Error', 'Failed to load historical data from database.', 'error');
}
},
"order": [[ 0, "desc" ]],
"pageLength": 10,
"lengthMenu": [[10, 25, 50, 100], [10, 25, 50, 100]],
"dom": '<"flex justify-between items-center mb-4"lf>rt<"flex justify-between items-center mt-4"ip>',
"language": {
"search": "",
"searchPlaceholder": "Search alerts...",
"lengthMenu": "_MENU_ per page"
},
"columns": [
{
"data": "created_at",
"render": function(data, type, row) {
if(type === 'display' || type === 'filter') {
let d = new Date(data);
return `<span class="text-slate-600">${d.toLocaleString('en-GB')}</span>`;
}
return data;
}
},
{
"data": "rule_level",
"render": function(data, type, row) {
let level = parseInt(data);
let color = 'bg-slate-100 text-slate-600 border-slate-200';
if (level >= 12) color = 'bg-rose-50 text-rose-600 border-rose-200';
else if (level >= 8) color = 'bg-amber-50 text-amber-600 border-amber-200';
else if (level >= 5) color = 'bg-blue-50 text-blue-600 border-blue-200';
return `<span class="px-2 py-1 rounded text-xs font-bold border ${color}">Level ${level}</span>`;
}
},
{
"data": "description",
"className": "whitespace-normal break-words max-w-md text-left",
"render": function(data, type, row) {
return `<span class="text-slate-700 font-medium">${data}</span>
<div class="text-[11px] text-slate-400 mt-0.5"><i class="fas fa-hashtag text-slate-300"></i> Rule ID: ${row.rule_id}</div>`;
}
},
{
"data": "agent_name",
"render": function(data, type, row) {
return `<span class="text-slate-600 font-mono text-xs bg-slate-100 px-1.5 py-0.5 rounded border border-slate-200">${data}</span>`;
}
},
{
"data": null,
"orderable": false,
"className": "text-center",
"render": function(data, type, row) {
// Embed raw_data safely in data-raw attribute
let safeJson = "";
try {
safeJson = JSON.stringify(row.raw_data).replace(/'/g, "&#39;");
} catch(e) {
safeJson = "{}";
}
return `<button onclick="viewDetails(this)" data-raw='${safeJson}' class="corp-btn-secondary px-3 py-1.5 rounded text-xs"><i class="fas fa-search mr-1"></i> View</button>`;
}
}
]
});
$('#btnFilter').on('click', function() {
table.ajax.reload();
});
$('#btnClearFilter').on('click', function() {
$('#startDate').val('');
$('#endDate').val('');
table.ajax.reload();
});
// Reuse the viewDetails function from dashboard
window.scanVirusTotal = function(type, value, btnElement) {
let originalHtml = btnElement.innerHTML;
btnElement.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Scanning...';
btnElement.disabled = true;
let container = btnElement.closest('.bg-slate-50').querySelector('#vt-result-container');
$.ajax({
url: '<?= $baseUrl ?>/api/virustotal-check',
method: 'POST',
data: { type: type, value: value },
success: function(res) {
if (res.status === 'success') {
container.innerHTML = res.html;
btnElement.innerHTML = '<i class="fas fa-check mr-1"></i> Scanned';
} else {
Swal.fire('Error', res.message, 'error');
btnElement.innerHTML = originalHtml;
btnElement.disabled = false;
}
},
error: function() {
Swal.fire('Error', 'Failed to connect to VT API.', 'error');
btnElement.innerHTML = originalHtml;
btnElement.disabled = false;
}
});
};
window.viewDetails = function(btnElement) {
try {
let rawDataStr = btnElement.getAttribute('data-raw');
if (rawDataStr.startsWith('"') && rawDataStr.endsWith('"')) {
rawDataStr = JSON.parse(rawDataStr);
}
let data = typeof rawDataStr === 'string' ? JSON.parse(rawDataStr) : rawDataStr;
let ruleId = data.rule ? data.rule.id : 'N/A';
let ruleDesc = data.rule ? data.rule.description : 'No Description';
let fullLog = data.full_log || 'N/A';
let agentIp = data.agent ? data.agent.ip : 'N/A';
// Advanced parsing for specific events like Windows
let extractThreatTarget = (data) => {
if (data.syscheck && data.syscheck.sha256_after) return { target: data.syscheck.sha256_after, type: 'hash' };
if (data.syscheck && data.syscheck.md5_after) return { target: data.syscheck.md5_after, type: 'hash' };
if (data.syscheck && data.syscheck.sha1_after) return { target: data.syscheck.sha1_after, type: 'hash' };
if (data.data && data.data.srcip && data.data.srcip !== '127.0.0.1') return { target: data.data.srcip, type: 'ip' };
if (data.data && data.data.destip && data.data.destip !== '127.0.0.1') return { target: data.data.destip, type: 'ip' };
return { target: null, type: null };
};
let threat = extractThreatTarget(data);
let vtHtml = '';
if (threat.target) {
let typeLabel = threat.type === 'hash' ? 'File Hash' : 'IP Address';
vtHtml = `
<div class="bg-slate-50 border border-slate-200 p-4 mt-4 rounded shadow-sm">
<strong class="text-slate-800 block mb-3 text-sm uppercase tracking-wide border-b border-slate-200 pb-2">
<i class="fas fa-shield-virus text-slate-400 mr-2"></i> Threat Intelligence (VirusTotal)
</strong>
<div class="flex items-center justify-between">
<div>
<span class="text-slate-500 text-xs font-bold uppercase block">${typeLabel}</span>
<span class="font-mono text-sm font-semibold text-slate-800 break-all">${threat.target}</span>
</div>
<button onclick="scanVirusTotal('${threat.type}', '${threat.target}', this)" class="bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1.5 rounded text-xs font-bold uppercase shadow transition shrink-0 ml-4">
<i class="fas fa-search mr-1"></i> Scan Now
</button>
</div>
<div id="vt-result-container"></div>
</div>`;
}
let extraDetailsHtml = '';
if (data.data && data.data.win && data.data.win.system) {
let win = data.data.win.system;
let evData = data.data.win.eventdata || {};
let paramsHtml = '';
Object.keys(evData).forEach(key => {
paramsHtml += `<tr><td class="py-1.5 px-3 border-b border-slate-100 text-slate-500 font-semibold w-1/3">${key}</td><td class="py-1.5 px-3 border-b border-slate-100 text-slate-800 break-words">${evData[key]}</td></tr>`;
});
extraDetailsHtml = `
<div class="bg-blue-50 border border-blue-100 p-4 rounded mt-4">
<strong class="text-blue-800 block mb-2 text-sm"><i class="fab fa-windows mr-2"></i> Windows Event Details</strong>
<div class="grid grid-cols-2 gap-2 text-xs mb-3">
<div><span class="text-slate-500 uppercase font-bold">Provider:</span> <span class="text-slate-800 font-medium">${win.providerName || 'N/A'}</span></div>
<div><span class="text-slate-500 uppercase font-bold">Event ID:</span> <span class="text-slate-800 font-medium">${win.eventID || 'N/A'}</span></div>
<div><span class="text-slate-500 uppercase font-bold">Channel:</span> <span class="text-slate-800 font-medium">${win.channel || 'N/A'}</span></div>
</div>
${paramsHtml ? `<table class="w-full text-left text-xs bg-white border border-slate-200 rounded overflow-hidden"><tbody>${paramsHtml}</tbody></table>` : ''}
</div>`;
}
let getRemediationHtml = (ruleId, desc, level) => {
return `
<div id="ai-remediation-container" class="bg-indigo-50 border border-indigo-100 p-4 mt-4 rounded shadow-sm relative overflow-hidden">
<div class="absolute inset-0 bg-indigo-100/50 animate-pulse" id="ai-loading-bg"></div>
<strong class="text-indigo-800 block mb-3 relative z-10 flex items-center border-b border-indigo-200 pb-2">
<i class="fas fa-robot mr-2 text-lg"></i> AI Investigation & Remediation
</strong>
<div id="ai-remediation-content" class="relative z-10 text-indigo-900 min-h-[40px] flex flex-col justify-center">
<div class="flex items-center text-sm font-medium text-indigo-600">
<i class="fas fa-circle-notch fa-spin text-indigo-500 mr-2 text-base"></i> Analyzing event ${ruleId} and generating recommendations...
</div>
</div>
</div>`;
};
let htmlContent = `
<div class="text-left">
<div class="mb-4">
<span class="text-xs font-bold px-2 py-1 bg-slate-100 text-slate-500 rounded border border-slate-200 mr-2">Rule: ${ruleId}</span>
<span class="text-xs font-bold px-2 py-1 ${data.rule && data.rule.level >= 12 ? 'bg-rose-100 text-rose-600 border-rose-200' : 'bg-blue-100 text-blue-600 border-blue-200'} rounded border">Level: ${data.rule ? data.rule.level : 'N/A'}</span>
</div>
<div class="bg-slate-800 text-white p-4 rounded-md mb-4 shadow-inner">
<p class="text-sm font-medium leading-relaxed">${ruleDesc}</p>
</div>
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<strong class="text-slate-800 block mb-3 text-sm uppercase tracking-wide border-b border-slate-200 pb-2"><i class="fas fa-server text-slate-400 mr-2"></i> Endpoint Details</strong>
<div class="grid grid-cols-2 gap-y-3 gap-x-4 mt-2">
<div><span class="text-slate-500 text-xs uppercase font-bold block mb-0.5">Hostname</span> <span class="font-medium text-slate-800">${data.agent.name}</span></div>
<div><span class="text-slate-500 text-xs uppercase font-bold block mb-0.5">IP Address</span> <span class="font-mono text-slate-800">${agentIp}</span></div>
<div><span class="text-slate-500 text-xs uppercase font-bold block mb-0.5">Agent ID</span> <span class="font-mono text-slate-800">${data.agent.id || 'N/A'}</span></div>
</div>
<div class="grid grid-cols-2 gap-y-3 gap-x-4 mt-3 pt-3 border-t border-slate-100">
<div>
<span class="text-slate-500 text-[10px] uppercase font-bold block mb-1"><i class="fas fa-barcode text-slate-400 mr-1"></i> Asset ID (เลขครุภัณฑ์)</span>
${data.agent && data.agent.labels && data.agent.labels.asset_id ?
`<span class="font-mono text-xs font-bold text-indigo-700 bg-indigo-50 px-2 py-1 rounded border border-indigo-200">${data.agent.labels.asset_id}</span>` :
`<span class="text-slate-400 text-xs italic">Not found</span>`}
</div>
<div>
<span class="text-slate-500 text-[10px] uppercase font-bold block mb-1"><i class="fas fa-map-marker-alt text-slate-400 mr-1"></i> Location (จุดติดตั้ง)</span>
${data.agent && data.agent.labels && data.agent.labels.location ?
`<span class="font-medium text-xs text-slate-700 bg-slate-100 px-2 py-1 rounded border border-slate-200">${data.agent.labels.location}</span>` :
`<span class="text-slate-400 text-xs italic">Not found</span>`}
</div>
</div>
<div id="endpoint-specs-container" class="mt-4 pt-3 border-t border-slate-200 hidden">
<span class="text-slate-500 text-xs uppercase font-bold block mb-2"><i class="fas fa-microchip mr-1"></i> Hardware Specifications</span>
<div id="endpoint-specs-content" class="text-xs text-slate-600 grid grid-cols-2 gap-2">
<span class="text-slate-400 italic"><i class="fas fa-circle-notch fa-spin mr-1"></i> Loading specs...</span>
</div>
</div>
</div>
${extraDetailsHtml}
${fullLog !== 'N/A' ? `
<div class="bg-slate-50 p-4 rounded border border-slate-200 mt-4">
<strong class="text-slate-800 block mb-3 text-sm uppercase tracking-wide border-b border-slate-200 pb-2"><i class="fas fa-file-alt text-slate-400 mr-2"></i> Original Raw Log</strong>
<div class="text-xs bg-white p-3 border border-slate-200 rounded font-mono text-slate-600 break-words overflow-x-auto">
${fullLog.replace(/</g, '&lt;').replace(/>/g, '&gt;')}
</div>
</div>
` : ''}
${vtHtml}\n ${getRemediationHtml(ruleId, ruleDesc, data.rule ? data.rule.level : 0)}
<details class="group bg-slate-50 border border-slate-200 rounded mt-4">
<summary class="cursor-pointer font-bold text-sm text-slate-700 uppercase tracking-wide p-3 select-none flex items-center justify-between">
<span><i class="fas fa-code text-slate-400 mr-2"></i> Developer Raw JSON</span>
<i class="fas fa-chevron-down text-slate-400 transition-transform group-open:rotate-180"></i>
</summary>
<div class="p-3 border-t border-slate-200">
<pre class="bg-slate-900 text-slate-300 p-3 rounded font-mono text-[10px] overflow-auto max-h-64 whitespace-pre-wrap">${JSON.stringify(data, null, 2)}</pre>
</div>
</details>
</div>
`;
Swal.fire({
title: '<span class="text-slate-800 font-bold text-lg border-b-2 border-blue-500 pb-1">Deep Event Inspection</span>',
html: htmlContent,
icon: '',
confirmButtonText: 'Close Window',
buttonsStyling: false,
customClass: {
popup: 'corp-card rounded-md',
confirmButton: 'corp-btn-primary px-6 py-2.5 rounded text-sm font-medium mt-4 w-full'
},
width: '750px'
});
// Fetch Hardware Specs via AJAX if Agent ID is available
if (data.agent && data.agent.id) {
document.getElementById('endpoint-specs-container').classList.remove('hidden');
$.ajax({
url: '<?= $baseUrl ?>/api/agent/quick-details?agent_id=' + data.agent.id,
method: 'GET',
success: function(res) {
let content = document.getElementById('endpoint-specs-content');
let html = '';
if (res.hardware) {
let hw = res.hardware;
// Support both Wazuh 3.x (flat) and 4.x (nested) hardware structures
let cpuName = hw.cpu_name || (hw.cpu && hw.cpu.name) || 'N/A';
let ramTotal = hw.ram_total || (hw.ram && hw.ram.total) || 0;
let ramFree = hw.ram_free || (hw.ram && hw.ram.free) || 0;
let ramTotalGB = ramTotal ? (ramTotal / 1024 / 1024).toFixed(1) + ' GB' : 'N/A';
let ramUsedHTML = 'N/A';
if (ramTotal > 0 && ramFree > 0) {
let ramUsed = ramTotal - ramFree;
let ramUsedPercent = Math.round((ramUsed / ramTotal) * 100);
let color = ramUsedPercent > 85 ? 'text-rose-600' : (ramUsedPercent > 70 ? 'text-amber-500' : 'text-emerald-600');
ramUsedHTML = `<span class="font-medium text-slate-800">${ramTotalGB}</span> (<span class="${color} font-bold">${ramUsedPercent}% Used</span>)`;
} else {
ramUsedHTML = `<span class="font-medium text-slate-800">${ramTotalGB}</span>`;
}
html += `
<div><span class="text-slate-400 font-semibold">CPU:</span> <span class="font-medium text-slate-800" title="${cpuName}">${cpuName.length > 25 ? cpuName.substring(0, 25) + '...' : cpuName}</span></div>
<div><span class="text-slate-400 font-semibold">RAM:</span> ${ramUsedHTML}</div>
`;
} else if (res.error) {
html += `<div class="col-span-2 text-rose-500 text-[10px] break-words">HW Error: ${res.error}</div>`;
}
// Indexer Status Warning
if (res.indexer_status && res.indexer_status !== 'OK') {
html += `<div class="col-span-2 mt-2 bg-amber-50 border border-amber-200 text-amber-700 p-2 rounded text-[10px]">
<i class="fas fa-exclamation-triangle mr-1"></i> <b>Database Warning:</b> Cannot connect to Wazuh Indexer (Status: ${res.indexer_status}). Please check .env credentials. Disk and Remote info will be unavailable.
</div>`;
}
// Parse Custom Inventory Data
if (res.model_info && res.model_info.PCModel) {
html += `<div><span class="text-slate-400 font-semibold">Model:</span> <span class="font-medium text-slate-800 truncate" title="${res.model_info.PCModel}">${res.model_info.PCModel}</span></div>`;
}
if (res.manufacturer_info && res.manufacturer_info.PCManufacturer) {
html += `<div><span class="text-slate-400 font-semibold">Brand:</span> <span class="font-medium text-slate-800">${res.manufacturer_info.PCManufacturer}</span></div>`;
}
// Parse Disk
if (res.disk_info) {
let disks = Array.isArray(res.disk_info) ? res.disk_info : [res.disk_info];
for (let d of disks) {
let dLetter = d.DriveLetter || d.Drive || d.DeviceID || d.Name || '';
if (dLetter.toUpperCase().includes('C')) {
let total = parseFloat(d.Total_GB || d.SizeGB || (d.Size ? (d.Size/1073741824).toFixed(2) : 0));
let free = parseFloat(d.Free_GB || d.FreeGB || (d.FreeSpace ? (d.FreeSpace/1073741824).toFixed(2) : 0));
if (total > 0) {
let used = total - free;
let usedPercent = Math.round((used / total) * 100);
let freePercent = Math.round((free / total) * 100);
let color = freePercent < 15 ? 'text-rose-600' : 'text-emerald-600';
html += `<div class="col-span-2 mt-1 bg-slate-100 p-2 rounded border border-slate-200">
<div class="flex justify-between mb-1">
<span class="text-slate-500 font-bold text-[11px]"><i class="fas fa-hdd mr-1"></i> Drive C: Space</span>
<span class="${color} font-bold text-[11px]">${freePercent}% Free</span>
</div>
<div class="w-full bg-slate-300 rounded-full h-1.5 mb-1 overflow-hidden">
<div class="bg-blue-500 h-1.5 rounded-full ${usedPercent > 85 ? '!bg-rose-500' : ''}" style="width: ${usedPercent}%"></div>
</div>
<div class="flex justify-between text-[10px] text-slate-500">
<span>Used: ${used.toFixed(1)} GB (${usedPercent}%)</span>
<span>Total: ${total.toFixed(1)} GB</span>
</div>
</div>`;
}
break;
}
}
}
// Remote Tools
let remoteHtml = [];
if (res.tv_info && res.tv_info.TeamViewerID && res.tv_info.TeamViewerID !== 'Not Found') {
remoteHtml.push(`<span class="inline-flex items-center text-[11px]"><img src="https://www.teamviewer.com/favicon.ico" class="w-3 h-3 mr-1 opacity-80 rounded-sm"> <span class="font-medium text-slate-700">${res.tv_info.TeamViewerID}</span></span>`);
}
if (res.anydesk_info && res.anydesk_info.AnyDeskID && res.anydesk_info.AnyDeskID !== 'Not Found') {
remoteHtml.push(`<span class="inline-flex items-center text-[11px] ml-3"><img src="https://anydesk.com/favicon.ico" class="w-3 h-3 mr-1 opacity-80 rounded-sm"> <span class="font-medium text-slate-700">${res.anydesk_info.AnyDeskID}</span></span>`);
}
if (remoteHtml.length > 0) {
html += `<div class="col-span-2 mt-1"><span class="text-slate-400 font-semibold mr-1">Remote ID:</span> ${remoteHtml.join('')}</div>`;
}
// Network interfaces
if (res.netiface && res.netiface.length > 0) {
let activeIface = null;
// Find the first interface with state=up, not loopback, not virtual
for (let net of res.netiface) {
let name = (net.name || '').toLowerCase();
if (net.state === 'up' && net.mac && net.mac !== '00:00:00:00:00:00' && !name.includes('loopback') && !name.includes('pseudo')) {
activeIface = net;
// Prefer WiFi if found active
if (name.includes('wi-fi') || name.includes('wlan') || name.includes('wireless')) break;
}
}
if (activeIface) {
let nameLower = activeIface.name.toLowerCase();
let isWifi = nameLower.includes('wi-fi') || nameLower.includes('wlan') || nameLower.includes('wireless');
let netType = isWifi ? 'WiFi' : 'LAN';
let icon = isWifi ? 'fa-wifi text-blue-500' : 'fa-ethernet text-slate-500';
html += `<div class="col-span-2 mt-1"><span class="text-slate-400 font-semibold">Network:</span> <span class="font-medium text-slate-800"><i class="fas ${icon} mr-1"></i> ${netType} <span class="text-slate-400 text-[10px] font-normal">(${activeIface.name})</span></span></div>`;
}
}
content.innerHTML = html || '<span class="text-slate-400 col-span-2">No hardware or custom inventory specs available.</span>';
},
error: function() {
let content = document.getElementById('endpoint-specs-content');
if(content) content.innerHTML = '<span class="text-rose-500 col-span-2">Failed to load specs from server.</span>';
}
});
}
$.ajax({
url: '<?= $baseUrl ?>/api/ai-remediation',
method: 'POST',
data: {
rule_id: ruleId,
rule_desc: ruleDesc,
level: data.rule ? data.rule.level : 0
},
success: function(res) {
$('#ai-loading-bg').remove();
if(res.status === 'success') {
$('#ai-remediation-content').html(res.html).hide().fadeIn(400);
} else {
let errorMsg = res.message || 'Failed to generate AI recommendations.';
$('#ai-remediation-content').html('<span class="text-rose-600"><i class="fas fa-exclamation-triangle mr-1"></i> ' + errorMsg + '</span>');
}
},
error: function() {
$('#ai-loading-bg').remove();
$('#ai-remediation-content').html('<span class="text-rose-600"><i class="fas fa-exclamation-triangle mr-1"></i> Error connecting to AI engine.</span>');
}
});
} catch(e) {
console.error(e);
Swal.fire('Error', 'Could not parse raw data.', 'error');
}
};
});
</script>
<?php
$content = ob_get_clean();
require_once __DIR__ . '/layout.php';
?>
@@ -0,0 +1,298 @@
<?php $pageTitle = __('System Settings'); ob_start(); ?>
<div class="max-w-7xl mx-auto mt-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Integration Configuration -->
<div class="corp-card p-6 bg-white rounded-xl shadow-sm border border-slate-200">
<h3 class="text-lg font-bold text-slate-800 mb-6 border-b border-slate-200 pb-3 flex items-center">
<i class="fas fa-server text-slate-400 mr-2"></i> <?= __('Core Integration') ?>
</h3>
<div class="space-y-5">
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<label class="block text-slate-600 text-xs font-bold mb-1 uppercase tracking-wide"><?= __('Wazuh Manager API URL') ?></label>
<input type="text" value="<?= htmlspecialchars($_ENV['WAZUH_API_URL'] ?? '') ?>" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-slate-500 bg-white cursor-not-allowed font-mono text-sm shadow-sm">
</div>
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<label class="block text-slate-600 text-xs font-bold mb-1 uppercase tracking-wide"><?= __('Wazuh Indexer URL') ?></label>
<input type="text" value="<?= htmlspecialchars($_ENV['WAZUH_INDEXER_URL'] ?? '') ?>" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-slate-500 bg-white cursor-not-allowed font-mono text-sm shadow-sm">
</div>
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<label class="block text-slate-600 text-xs font-bold mb-1 uppercase tracking-wide"><?= __('System Name (APP_NAME)') ?></label>
<input type="text" value="<?= htmlspecialchars($_ENV['APP_NAME'] ?? 'KSH SOAR') ?>" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-slate-800 font-semibold bg-white cursor-not-allowed text-sm shadow-sm">
</div>
</div>
</div>
<!-- System Configuration -->
<div class="corp-card p-6 bg-white rounded-xl shadow-sm border border-slate-200">
<h3 class="text-lg font-bold text-slate-800 mb-6 border-b border-slate-200 pb-3 flex items-center">
<i class="fas fa-sliders-h text-slate-400 mr-2"></i> <?= __('Rule & Retention Policies') ?>
</h3>
<div class="space-y-5">
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<label class="block text-slate-600 text-xs font-bold mb-1 uppercase tracking-wide"><?= __('Alert Retention Policy') ?></label>
<input type="text" value="<?= htmlspecialchars($_ENV['ALERT_RETENTION_DAYS'] ?? 90) ?> Days" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-slate-800 font-semibold bg-white cursor-not-allowed text-sm shadow-sm">
</div>
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<label class="block text-slate-600 text-xs font-bold mb-1 uppercase tracking-wide"><?= __('Alert Level Threshold') ?></label>
<input type="text" value="Level <?= htmlspecialchars($_ENV['MIN_ALERT_LEVEL'] ?? 7) ?>+" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-slate-800 font-semibold bg-white cursor-not-allowed text-sm shadow-sm">
<p class="text-[10px] text-slate-400 mt-1">Configure <code class="bg-slate-200 px-1 rounded">MIN_ALERT_LEVEL</code> in .env</p>
</div>
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<label class="block text-slate-600 text-xs font-bold mb-1 uppercase tracking-wide"><?= __('Excluded Rule IDs (Ignored Alerts)') ?></label>
<?php
$excluded = $_ENV['EXCLUDE_RULE_IDS'] ?? '';
$displayExcluded = empty($excluded) ? 'None (Log all rules)' : $excluded;
?>
<input type="text" value="<?= htmlspecialchars($displayExcluded) ?>" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-slate-800 font-semibold bg-white cursor-not-allowed text-sm shadow-sm">
<p class="text-[10px] text-slate-400 mt-1">Configure <code class="bg-slate-200 px-1 rounded">EXCLUDE_RULE_IDS</code> in .env (Comma separated)</p>
</div>
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<label class="block text-slate-600 text-xs font-bold mb-1 uppercase tracking-wide"><?= __('Resource Alert Threshold (RAM & Disk)') ?></label>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-[10px] text-slate-500 font-bold mb-1"><?= __('RAM Alert Threshold (%)') ?></p>
<input type="text" value="<?= htmlspecialchars($_ENV['ALERT_RAM_PERCENT'] ?? '85') ?> %" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-rose-600 font-bold bg-white cursor-not-allowed text-sm shadow-sm text-center">
<p class="text-[10px] text-slate-400 mt-1 text-center"><code class="bg-slate-200 px-1 rounded">ALERT_RAM_PERCENT</code></p>
</div>
<div>
<p class="text-[10px] text-slate-500 font-bold mb-1"><?= __('Disk Alert Threshold (%)') ?></p>
<input type="text" value="<?= htmlspecialchars($_ENV['ALERT_DISK_PERCENT'] ?? '85') ?> %" disabled class="border border-slate-300 rounded w-full py-2 px-3 text-rose-600 font-bold bg-white cursor-not-allowed text-sm shadow-sm text-center">
<p class="text-[10px] text-slate-400 mt-1 text-center"><code class="bg-slate-200 px-1 rounded">ALERT_DISK_PERCENT</code></p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <?= __('Notification Playbooks') ?> -->
<div class="corp-card p-6 bg-white rounded-xl shadow-sm border border-slate-200 mb-6">
<h4 class="text-lg font-bold text-slate-800 mb-4 border-b border-slate-200 pb-3 flex items-center">
<i class="fas fa-bell text-slate-400 mr-2"></i> <?= __('Notification Playbooks') ?>
</h4>
<div class="bg-blue-50/50 border border-blue-100 rounded p-3 mb-4 flex items-start text-sm text-blue-800">
<i class="fas fa-info-circle text-blue-500 mt-0.5 mr-2"></i>
<div>
<strong class="text-blue-900 font-semibold">กฎการแจ้งเตือนอัตโนมัติ:</strong>
ระบบจะส่งข้อความแจ้งเตือนเข้าแชทเฉพาะเมื่อตรวจพบภัยคุกคาม <b>Level 12 ขึ้นไป</b> (High / Critical) เพื่อป้องกันข้อความแจ้งเตือนเยอะเกินความจำเป็น
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- LINE Notify -->
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<div class="flex justify-between items-center mb-2">
<label class="text-slate-600 text-xs font-bold uppercase tracking-wide"><i class="fab fa-line text-green-500 mr-1"></i> LINE Notify</label>
<?php if (!empty($_ENV['LINE_NOTIFY_TOKEN'])): ?>
<span class="bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded text-[10px] font-bold uppercase">Configured</span>
<?php else: ?>
<span class="bg-rose-100 text-rose-700 px-2 py-0.5 rounded text-[10px] font-bold uppercase">Not Set</span>
<?php endif; ?>
</div>
<div class="flex items-center gap-2 mt-3">
<button onclick="testNotification(this, 'line')" class="corp-btn-secondary px-3 py-1.5 rounded text-xs w-full" <?= empty($_ENV['LINE_NOTIFY_TOKEN']) ? 'disabled opacity-50 cursor-not-allowed' : '' ?>>
<i class="fas fa-paper-plane mr-1"></i> Test LINE
</button>
</div>
</div>
<!-- Telegram Bot -->
<div class="bg-slate-50 p-4 rounded border border-slate-200">
<div class="flex justify-between items-center mb-2">
<label class="text-slate-600 text-xs font-bold uppercase tracking-wide"><i class="fab fa-telegram text-blue-500 mr-1"></i> Telegram Bot</label>
<?php if (!empty($_ENV['TELEGRAM_BOT_TOKEN']) && !empty($_ENV['TELEGRAM_CHAT_ID'])): ?>
<span class="bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded text-[10px] font-bold uppercase">Configured</span>
<?php else: ?>
<span class="bg-rose-100 text-rose-700 px-2 py-0.5 rounded text-[10px] font-bold uppercase">Not Set</span>
<?php endif; ?>
</div>
<div class="flex items-center gap-2 mt-3">
<button onclick="testNotification(this, 'telegram')" class="corp-btn-secondary px-3 py-1.5 rounded text-xs w-full" <?= (empty($_ENV['TELEGRAM_BOT_TOKEN']) || empty($_ENV['TELEGRAM_CHAT_ID'])) ? 'disabled opacity-50 cursor-not-allowed' : '' ?>>
<i class="fas fa-paper-plane mr-1"></i> Test Telegram
</button>
</div>
</div>
</div>
</div>
<!-- <?= __('Data Management') ?> -->
<div class="corp-card p-6 bg-white rounded-xl shadow-sm border border-slate-200 mb-6">
<h4 class="text-lg font-bold text-slate-800 mb-4 border-b border-slate-200 pb-3 flex items-center">
<i class="fas fa-database text-slate-400 mr-2"></i> <?= __('Data Management') ?>
</h4>
<div class="bg-rose-50 p-5 rounded border border-rose-200 mb-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center">
<div>
<h5 class="text-rose-800 font-bold text-sm"><?= __('Clear All Alerts') ?></h5>
<p class="text-rose-600 text-xs mt-1">This will permanently delete all alert records from the database. Cases and playbooks will not be deleted. This action cannot be undone.</p>
</div>
<button onclick="confirmClearAll()" class="mt-4 md:mt-0 bg-white text-rose-600 hover:bg-rose-600 hover:text-white border border-rose-300 hover:border-rose-600 px-5 py-2.5 rounded text-xs font-bold uppercase tracking-wide transition-colors whitespace-nowrap shadow-sm">
<i class="fas fa-trash-alt mr-1"></i> <?= __('Delete All Alerts') ?>
</button>
</div>
</div>
<?php if (!empty($deletionLogs)): ?>
<div>
<h5 class="text-slate-700 font-bold text-sm mb-3"><i class="fas fa-history text-slate-400 mr-1"></i> <?= __('Deletion History') ?></h5>
<div class="overflow-x-auto border border-slate-200 rounded-lg">
<table id="deletionHistoryTable" class="w-full text-left text-sm whitespace-nowrap">
<thead class="bg-slate-50 text-slate-500 text-xs uppercase">
<tr>
<th class="px-4 py-3 font-semibold"><?= __('Date Deleted') ?></th>
<th class="px-4 py-3 font-semibold"><?= __('Reason') ?></th>
<th class="px-4 py-3 font-semibold"><?= __('Records Deleted') ?></th>
<th class="px-4 py-3 font-semibold"><?= __('Data Date Range') ?></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white">
<?php foreach ($deletionLogs as $log): ?>
<tr class="hover:bg-slate-50">
<td class="px-4 py-3 text-slate-500 font-mono text-xs"><?= date('Y-m-d H:i', strtotime($log['created_at'])) ?></td>
<td class="px-4 py-3 text-slate-700 font-medium whitespace-normal break-words max-w-xs"><?= htmlspecialchars($log['reason']) ?></td>
<td class="px-4 py-3 text-rose-600 font-bold"><?= number_format($log['deleted_count']) ?></td>
<td class="px-4 py-3 text-slate-500 text-xs"><?= htmlspecialchars($log['date_range']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<!-- Notice -->
<div class="bg-slate-100 border-l-4 border-slate-400 p-4 text-sm text-slate-600 rounded mb-6">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-info-circle text-slate-500 mt-0.5"></i>
</div>
<div class="ml-3">
<p class="font-medium"><?= __('Configuration Management') ?></p>
<p class="mt-1">For security and compliance, system configurations are managed via environment variables (<code class="bg-slate-200 px-1 py-0.5 rounded text-xs">.env</code>) and cannot be modified through the web interface.</p>
</div>
</div>
</div>
</div>
<script>
function testNotification(btn, type) {
let originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Sending...';
btn.disabled = true;
$.ajax({
url: '<?= $baseUrl ?>/api/test-notification',
method: 'POST',
data: { type: type },
success: function(res) {
if (res.status === 'success') {
Swal.fire('Success', res.message, 'success');
} else {
Swal.fire('Error', res.message, 'error');
}
},
error: function() {
Swal.fire('Error', 'Failed to connect to the server.', 'error');
},
complete: function() {
btn.innerHTML = originalHtml;
btn.disabled = false;
}
});
}
function confirmClearAll() {
Swal.fire({
title: 'Authentication Required',
html: `You are about to <b class="text-rose-600">permanently delete ALL alerts</b>.<br>Please enter a reason and authorization password to confirm.
<div class="mt-4">
<input id="swal-reason" autocomplete="off" class="swal2-input !w-full !m-0 !mt-2 text-sm" placeholder="ระบุเหตุผลในการลบข้อมูล (จำเป็น)">
<input id="swal-password" type="password" autocomplete="new-password" class="swal2-input !w-full !m-0 !mt-3 text-sm" placeholder="ใส่รหัสผ่านเพื่อยืนยัน (CLEAR_ALERTS_PASSWORD)">
</div>`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#e11d48',
cancelButtonColor: '#64748b',
confirmButtonText: '<i class="fas fa-trash-alt mr-1"></i> Verify & Delete',
focusConfirm: false,
preConfirm: () => {
const reason = document.getElementById('swal-reason').value;
const password = document.getElementById('swal-password').value;
if (!reason) {
Swal.showValidationMessage('กรุณาระบุเหตุผลในการลบข้อมูล');
return false;
}
if (!password) {
Swal.showValidationMessage('กรุณาใส่รหัสผ่านเพื่อยืนยัน');
return false;
}
return { reason: reason, password: password };
}
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({
title: 'Deleting...',
text: 'Please wait while alerts are being removed',
allowOutsideClick: false,
didOpen: () => { Swal.showLoading(); }
});
$.ajax({
url: '<?= $baseUrl ?>/api/clear-all-alerts',
method: 'POST',
data: {
password: result.value.password,
reason: result.value.reason
},
success: function(res) {
if (res.status === 'success') {
Swal.fire('Deleted!', `Successfully removed ${res.deleted.toLocaleString()} alerts.`, 'success')
.then(() => { window.location.reload(); });
} else {
Swal.fire('Error', res.message, 'error');
}
},
error: function(xhr) {
let msg = 'Failed to communicate with the server.';
if (xhr.responseJSON && xhr.responseJSON.message) {
msg = xhr.responseJSON.message;
}
Swal.fire('Error', msg, 'error');
}
});
}
});
}
$(document).ready(function() {
if($('#deletionHistoryTable').length) {
$('#deletionHistoryTable').DataTable({
responsive: true,
order: [[0, 'desc']], // Sort by Date Deleted descending
pageLength: 5,
lengthMenu: [[5, 10, 25, 50, -1], [5, 10, 25, 50, "All"]],
language: {
search: "",
searchPlaceholder: "Search history..."
},
dom: '<"flex justify-between items-center mb-4 px-4 pt-4"lf>rt<"flex justify-between items-center mt-4 px-4 pb-4"ip>'
});
}
});
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/layout.php';
?>
@@ -0,0 +1,126 @@
<?php
$pageTitle = "Sysmon / EDR Dashboard";
ob_start();
?>
<div class="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-slate-800"><i class="fas fa-desktop text-indigo-600 mr-2"></i> Sysmon / EDR Dashboard</h1>
<p class="text-slate-500 mt-1">Endpoint Detection and Response data collected via Sysmon</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex items-center">
<div class="rounded-full bg-indigo-100 p-3 mr-4">
<i class="fas fa-microchip text-indigo-600 text-2xl"></i>
</div>
<div>
<div class="text-slate-500 text-sm font-semibold uppercase tracking-wider mb-1">Sysmon Alerts Today</div>
<div class="text-3xl font-bold text-slate-800"><?= number_format($totalSysmonToday) ?></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:col-span-2">
<h3 class="text-slate-700 font-semibold mb-3 border-b border-slate-100 pb-2"><i class="fas fa-exclamation-triangle text-amber-500 mr-2"></i> Top 5 Triggered Rules (Sysmon)</h3>
<?php if (empty($topRules)): ?>
<div class="text-slate-400 text-sm py-2">No sysmon alerts detected yet.</div>
<?php else: ?>
<div class="space-y-3">
<?php foreach ($topRules as $r): ?>
<div class="flex justify-between items-center text-sm">
<span class="text-slate-600 truncate mr-4"><span class="font-mono text-xs text-slate-400 mr-2"><?= htmlspecialchars($r['rule_id']) ?></span> <?= htmlspecialchars($r['description']) ?></span>
<span class="bg-amber-100 text-amber-700 py-0.5 px-2.5 rounded-full font-bold text-xs"><?= number_format($r['cnt']) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div class="p-5 border-b border-slate-100 bg-slate-50 flex justify-between items-center">
<h3 class="font-semibold text-slate-800"><i class="fas fa-list-ul mr-2 text-slate-400"></i> Recent Sysmon Events</h3>
</div>
<div class="overflow-x-auto">
<table id="sysmonTable" class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-50 text-slate-600 border-b border-slate-200 text-sm">
<th class="px-5 py-4 font-semibold w-40 text-center">Timestamp</th>
<th class="px-5 py-4 font-semibold text-center">Level</th>
<th class="px-5 py-4 font-semibold">Agent</th>
<th class="px-5 py-4 font-semibold">Rule / Description</th>
<th class="px-5 py-4 font-semibold text-center w-24">Details</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 text-sm">
<?php foreach ($recentAlerts as $a): ?>
<tr class="hover:bg-slate-50 transition-colors">
<td class="px-5 py-4 text-center text-slate-500 whitespace-nowrap text-xs"><?= date('Y-m-d H:i:s', strtotime($a['created_at'])) ?></td>
<td class="px-5 py-4 text-center">
<?php
$lvl = (int)$a['rule_level'];
$color = 'bg-slate-100 text-slate-600';
if ($lvl >= 12) $color = 'bg-red-100 text-red-700';
elseif ($lvl >= 7) $color = 'bg-amber-100 text-amber-700';
elseif ($lvl >= 5) $color = 'bg-blue-100 text-blue-700';
?>
<span class="px-2 py-1 rounded text-xs font-bold <?= $color ?>">L<?= $lvl ?></span>
</td>
<td class="px-5 py-4 font-semibold text-slate-700">
<?= htmlspecialchars($a['agent_name']) ?>
</td>
<td class="px-5 py-4">
<div class="font-mono text-xs text-slate-400 mb-0.5">ID: <?= htmlspecialchars($a['rule_id']) ?></div>
<div class="text-slate-700 font-medium"><?= htmlspecialchars($a['description']) ?></div>
</td>
<td class="px-5 py-4 text-center">
<button onclick="viewSysmonDetails(<?= $a['id'] ?>)" class="text-indigo-600 hover:text-indigo-900 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded transition">
<i class="fas fa-search"></i>
</button>
<template id="raw-<?= $a['id'] ?>">
<?= htmlspecialchars($a['raw_data']) ?>
</template>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script>
window.viewSysmonDetails = function(id) {
let raw = document.getElementById('raw-' + id).innerHTML;
try {
let jsonObj = JSON.parse(raw);
raw = JSON.stringify(jsonObj, null, 2);
} catch(e) {}
Swal.fire({
title: 'Sysmon Event Details',
html: `<pre class="text-left bg-slate-900 text-green-400 p-4 rounded-lg overflow-x-auto text-xs font-mono whitespace-pre-wrap">${raw}</pre>`,
width: '900px',
showCloseButton: true,
showConfirmButton: false
});
};
$(document).ready(function() {
$('#sysmonTable').DataTable({
responsive: true,
order: [[0, 'desc']],
pageLength: 25,
language: {
search: "",
searchPlaceholder: "Search Sysmon events..."
},
dom: '<"flex flex-col md:flex-row justify-between items-center mb-4 px-5 pt-4"lf>rt<"flex flex-col md:flex-row justify-between items-center mt-4 px-5 pb-4"ip>'
});
});
</script>
<?php
$content = ob_get_clean();
require 'layout.php';
?>