Files
2026-09-16 23:20:08 +07:00

493 lines
30 KiB
PHP

<?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';
?>