658 lines
40 KiB
PHP
658 lines
40 KiB
PHP
<?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, "'"); } 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, '<').replace(/>/g, '>')}
|
|
</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';
|
|
?>
|