Files
gravity/ระบบ SOAR integration ร่วมกับ Wazuh/views/dashboard.php
T
2026-09-16 23:20:08 +07:00

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, "&#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';
?>