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

1877 lines
111 KiB
PHP

<?php
session_start();
// ==========================================
// 1. ตั้งค่าการเชื่อมต่อ Wazuh API
// ==========================================
$wazuh_ip = 'wazuh.samuihospital.go.th'; // เปลี่ยนเป็น IP ของ Wazuh Manager ของคุณ
$wazuh_port = '55000';
$api_user = 'api_inventory'; // Username ของ Wazuh API
$api_pass = '@Samui@10742'; // Password ของ Wazuh API
$api_url = "https://{$wazuh_ip}:{$wazuh_port}";
// ==========================================
// 2. ตั้งค่าการเชื่อมต่อ Wazuh Indexer (ดึง Log Disk)
// ==========================================
$indexer_ip = $wazuh_ip; // ปกติจะอยู่ที่ Server เดียวกัน
$indexer_port = '9200';
$indexer_user = 'adminksh'; // User ของ Indexer (ค่าเริ่มต้นคือ admin)
$indexer_pass = '@Samui@10742'; // Password ของ Indexer (ค่าเริ่มต้น หรือที่คุณตั้งไว้)
if (isset($_GET['refresh'])) {
unset($_SESSION['wazuh_token']);
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
exit;
}
function call_wazuh_api($url, $method = 'GET', $token = null, $auth = null)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$headers = ["Content-Type: application/json"];
if ($auth) {
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, $auth);
}
if ($token) {
$headers[] = "Authorization: Bearer {$token}";
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!$response || $httpcode >= 400) return null;
return json_decode($response, true);
}
function get_wazuh_token()
{
global $api_url, $api_user, $api_pass;
if (!isset($_SESSION['wazuh_token'])) {
$auth = call_wazuh_api("{$api_url}/security/user/authenticate", 'POST', null, "{$api_user}:{$api_pass}");
if ($auth && isset($auth['data']['token'])) {
$_SESSION['wazuh_token'] = $auth['data']['token'];
} else {
return null;
}
}
return $_SESSION['wazuh_token'];
}
function check_indexer_connection()
{
global $indexer_ip, $indexer_port, $indexer_user, $indexer_pass;
$url = "https://{$indexer_ip}:{$indexer_port}/";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_USERPWD, "{$indexer_user}:{$indexer_pass}");
curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpcode == 200) return 'OK';
if ($httpcode == 401) return 'AUTH_FAILED';
return 'CONNECTION_ERROR';
}
function get_custom_inventory_from_indexer($agent_id, $location)
{
global $indexer_ip, $indexer_port, $indexer_user, $indexer_pass;
$url = "https://{$indexer_ip}:{$indexer_port}/wazuh-alerts-*/_search";
$payload = json_encode([
'size' => 1,
'sort' => ['@timestamp' => 'desc'],
'query' => ['bool' => ['must' => [['match' => ['agent.id' => $agent_id]], ['match' => ['location' => $location]]]]]
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_USERPWD, "{$indexer_user}:{$indexer_pass}");
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$data = json_decode($response, true);
if (isset($data['hits']['hits'][0]['_source']['full_log'])) {
$log = $data['hits']['hits'][0]['_source']['full_log'];
$log = preg_replace('/^ossec: output: \'' . preg_quote($location, '/') . '\':\s*/', '', $log);
// ป้องกัน json_decode พังถ้ารูปแบบผิด
$json = json_decode($log, true);
return $json !== null ? $json : $log;
}
}
return null;
}
function get_indexer_metadata()
{
global $indexer_ip, $indexer_port, $indexer_user, $indexer_pass;
$url = "https://{$indexer_ip}:{$indexer_port}/wazuh-alerts-*/_search";
$payload = json_encode([
'size' => 10000,
'sort' => ['@timestamp' => 'desc'],
'query' => ['terms' => ['location' => ['teamviewer_inventory', 'anydesk_inventory', 'disk_inventory', 'model_inventory', 'manufacturer_inventory']]],
'_source' => ['agent.id', 'location', 'full_log', 'agent.labels']
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_USERPWD, "{$indexer_user}:{$indexer_pass}");
$response = curl_exec($ch);
curl_close($ch);
$metadata = [];
if ($response) {
$data = json_decode($response, true);
if (isset($data['hits']['hits'])) {
foreach ($data['hits']['hits'] as $hit) {
$src = $hit['_source'];
$aid = $src['agent']['id'] ?? '';
$loc = $src['location'] ?? '';
$log = $src['full_log'] ?? '';
$labels = $src['agent']['labels'] ?? null;
if (!$aid) continue;
if (!isset($metadata[$aid])) {
$metadata[$aid] = ['tv' => null, 'ad' => null, 'asset_id' => null, 'location' => null, 'disk_c_total' => null, 'disk_c_free' => null, 'model' => null, 'manufacturer' => null];
}
if ($labels) {
if ($metadata[$aid]['asset_id'] === null && isset($labels['asset_id'])) $metadata[$aid]['asset_id'] = $labels['asset_id'];
if ($metadata[$aid]['location'] === null && isset($labels['location'])) $metadata[$aid]['location'] = $labels['location'];
}
if ($loc === 'teamviewer_inventory' && $metadata[$aid]['tv'] === null) {
$log = preg_replace('/^ossec: output: \'teamviewer_inventory\':\s*/', '', $log);
$json = json_decode($log, true);
$metadata[$aid]['tv'] = ($json && isset($json['TeamViewerID']) && $json['TeamViewerID'] !== 'Not Found') ? $json['TeamViewerID'] : 'None';
}
if ($loc === 'anydesk_inventory' && $metadata[$aid]['ad'] === null) {
$log = preg_replace('/^ossec: output: \'anydesk_inventory\':\s*/', '', $log);
$json = json_decode($log, true);
$metadata[$aid]['ad'] = ($json && isset($json['AnyDeskID']) && $json['AnyDeskID'] !== 'Not Found') ? $json['AnyDeskID'] : 'None';
}
// 🌟 อัปเดตตัวกรอง Disk C สำหรับฝั่ง PHP (ดึงเข้า Excel) 🌟
if ($loc === 'disk_inventory' && $metadata[$aid]['disk_c_total'] === null) {
$log = preg_replace('/^ossec: output: \'disk_inventory\':\s*/', '', $log);
$json = json_decode($log, true);
if ($json && !is_array($json) && is_object((object)$json)) {
$json = [$json];
}
if (is_array($json)) {
foreach ($json as $disk) {
// รองรับทั้งคีย์ DriveLetter, Drive, DeviceID, Name
$dLetter = $disk['DriveLetter'] ?? $disk['Drive'] ?? $disk['DeviceID'] ?? $disk['Name'] ?? '';
// เช็คให้มีตัวอักษร C ไม่ว่าจะมาแบบ "C:", "C:\", "C"
if (strpos(strtoupper($dLetter), 'C') !== false) {
// รองรับขนาดทั้งหน่วย GB และ Bytes
$t = $disk['Total_GB'] ?? $disk['SizeGB'] ?? (isset($disk['Size']) ? round($disk['Size'] / 1073741824, 2) : 0);
$f = $disk['Free_GB'] ?? $disk['FreeGB'] ?? (isset($disk['FreeSpace']) ? round($disk['FreeSpace'] / 1073741824, 2) : 0);
$metadata[$aid]['disk_c_total'] = $t ?: 'N/A';
$metadata[$aid]['disk_c_free'] = $f ?: 'N/A';
break;
}
}
}
}
if ($loc === 'model_inventory' && $metadata[$aid]['model'] === null) {
$log = preg_replace('/^ossec: output: \'model_inventory\':\s*/', '', $log);
$json = json_decode($log, true);
$metadata[$aid]['model'] = ($json && isset($json['PCModel'])) ? $json['PCModel'] : 'Unknown';
}
if ($loc === 'manufacturer_inventory' && $metadata[$aid]['manufacturer'] === null) {
$log = preg_replace('/^ossec: output: \'manufacturer_inventory\':\s*/', '', $log);
$json = json_decode($log, true);
$metadata[$aid]['manufacturer'] = ($json && isset($json['PCManufacturer'])) ? $json['PCManufacturer'] : 'Unknown';
}
}
}
}
return $metadata;
}
function format_os_name($agent)
{
if (($agent['id'] ?? '') === '000') return 'Wazuh Server';
$os_name = $agent['os']['name'] ?? $agent['os']['platform'] ?? 'Unknown';
$nameLower = strtolower($os_name);
$version = $agent['os']['version'] ?? '';
if (strpos($nameLower, 'windows 11') !== false) return 'Windows 11';
if (strpos($nameLower, 'windows 10') !== false) return 'Windows 10';
if (strpos($nameLower, 'windows 7') !== false) return 'Windows 7';
if (strpos($nameLower, 'windows 8') !== false) return 'Windows 8';
if (strpos($nameLower, 'windows server 2022') !== false) return 'Windows Server 2022';
if (strpos($nameLower, 'windows server 2019') !== false) return 'Windows Server 2019';
if (strpos($nameLower, 'windows server 2016') !== false) return 'Windows Server 2016';
if (strpos($nameLower, 'windows server 2012') !== false) return 'Windows Server 2012';
if (strpos($nameLower, 'ubuntu') !== false) {
if (preg_match('/Ubuntu (\d+\.\d+)/i', $os_name, $m)) return 'Ubuntu ' . $m[1];
return 'Ubuntu';
}
if (strpos($nameLower, 'centos') !== false) {
if (preg_match('/CentOS.*?(\d+)/i', $os_name, $m)) return 'CentOS ' . $m[1];
return 'CentOS';
}
if (strpos($nameLower, 'mac') !== false || strpos($nameLower, 'darwin') !== false) {
return $version ? 'macOS ' . $version : 'macOS';
}
return $os_name;
}
// ==========================================
// 3. ระบบ AJAX สำหรับดึงข้อมูล
// ==========================================
if (isset($_GET['action']) && $_GET['action'] === 'get_specs' && isset($_GET['agent_id'])) {
header('Content-Type: application/json');
$token = get_wazuh_token();
if (!$token) {
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$agent_id = htmlspecialchars($_GET['agent_id']);
$hw_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hardware", 'GET', $token);
$netiface_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/netiface", 'GET', $token);
$netaddr_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/netaddr", 'GET', $token);
$netproto_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/netproto", 'GET', $token);
$packages_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/packages?limit=2000", 'GET', $token);
$hotfixes_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hotfixes?limit=1000", 'GET', $token);
$disk_info = get_custom_inventory_from_indexer($agent_id, 'disk_inventory');
$tv_info = get_custom_inventory_from_indexer($agent_id, 'teamviewer_inventory');
$anydesk_info = get_custom_inventory_from_indexer($agent_id, 'anydesk_inventory');
echo json_encode([
'hardware' => $hw_res['data']['affected_items'][0] ?? null,
'disk_info' => $disk_info,
'tv_info' => $tv_info,
'anydesk_info' => $anydesk_info,
'netiface' => $netiface_res['data']['affected_items'] ?? [],
'netaddr' => $netaddr_res['data']['affected_items'] ?? [],
'netproto' => $netproto_res['data']['affected_items'] ?? [],
'packages' => $packages_res['data']['affected_items'] ?? [],
'hotfixes' => $hotfixes_res['data']['affected_items'] ?? []
]);
exit;
}
if (isset($_GET['action']) && $_GET['action'] === 'get_dashboard_data' && isset($_GET['agent_id'])) {
header('Content-Type: application/json');
$token = get_wazuh_token();
$agent_id = htmlspecialchars($_GET['agent_id']);
$hw_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hardware", 'GET', $token);
$packages_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/packages?limit=5000", 'GET', $token);
$hotfixes_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hotfixes?limit=2000", 'GET', $token);
echo json_encode(['hardware' => $hw_res['data']['affected_items'][0] ?? null, 'packages' => $packages_res['data']['affected_items'] ?? [], 'hotfixes' => $hotfixes_res['data']['affected_items'] ?? []]);
exit;
}
if (isset($_GET['action']) && $_GET['action'] === 'get_processes' && isset($_GET['agent_id'])) {
header('Content-Type: application/json');
$token = get_wazuh_token();
$agent_id = htmlspecialchars($_GET['agent_id']);
$processes_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/processes?limit=5000", 'GET', $token);
echo json_encode(['processes' => $processes_res['data']['affected_items'] ?? []]);
exit;
}
// ==========================================
// 4. ดึงข้อมูลหน้าหลัก
// ==========================================
$token = get_wazuh_token();
$indexer_status = check_indexer_connection();
$agents = [];
$all_metadata = [];
$unique_os_list = [];
if ($token) {
$agents_response = call_wazuh_api("{$api_url}/agents?limit=10000", 'GET', $token);
if ($agents_response && isset($agents_response['data']['affected_items'])) {
$agents = $agents_response['data']['affected_items'];
foreach ($agents as $agent) {
$formatted_os = format_os_name($agent);
$unique_os_list[$formatted_os] = true;
}
$unique_os_list = array_keys($unique_os_list);
sort($unique_os_list);
}
if ($indexer_status === 'OK') {
$all_metadata = get_indexer_metadata();
foreach ($agents as &$ag) {
$aid = $ag['id'];
if (isset($all_metadata[$aid])) {
if (!isset($ag['labels'])) $ag['labels'] = [];
if (isset($all_metadata[$aid]['asset_id'])) $ag['labels']['asset_id'] = $all_metadata[$aid]['asset_id'];
if (isset($all_metadata[$aid]['location'])) $ag['labels']['location'] = $all_metadata[$aid]['location'];
}
}
unset($ag);
}
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise Asset Management</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif']
},
colors: {
primary: '#2563EB',
secondary: '#475569',
success: '#059669',
warning: '#D97706',
danger: '#DC2626',
info: '#0284C7'
}
}
}
}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<style>
/* =========================================
🌟 Modern 3D & DataTables Layout Fixes 🌟
========================================= */
body {
background-color: #e2e8f0;
font-family: 'Sarabun', sans-serif !important;
}
/* 3D Card Hover Effects */
.glass-card {
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
border-color: rgba(37, 99, 235, 0.3);
}
.text-gradient {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to right, #1e3a8a, #3b82f6);
}
/* 🌟 DataTables Layout Correcting (จัดบรรทัด Search & Length) 🌟 */
.dataTables_wrapper {
width: 100%;
color: #334155;
font-family: 'Sarabun', sans-serif;
}
@media (min-width: 768px) {
.dataTables_wrapper .dataTables_length {
float: left;
margin-bottom: 1.5rem;
}
.dataTables_wrapper .dataTables_filter {
float: right;
margin-bottom: 1.5rem;
}
.dataTables_wrapper .dataTables_info {
float: left;
clear: both;
padding-top: 1.5rem;
}
.dataTables_wrapper .dataTables_paginate {
float: right;
padding-top: 1.5rem;
}
}
@media (max-width: 767px) {
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter {
float: none;
text-align: left;
margin-bottom: 1rem;
display: block;
}
}
.dataTables_wrapper::after {
content: "";
display: table;
clear: both;
}
/* 🌟 แก้ปัญหาลูกศรซ้อนและขยายช่อง Show Entries 🌟 */
.dataTables_wrapper .dataTables_length select {
-webkit-appearance: none !important;
-moz-appearance: none !important;
appearance: none !important;
background-color: #f8fafc;
border: 1px solid #cbd5e1;
color: #1e293b;
font-weight: 600;
border-radius: 0.5rem;
padding: 0.4rem 2.5rem 0.4rem 1rem !important;
min-width: 90px !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23475569' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;
background-repeat: no-repeat !important;
background-position: right 0.75rem center !important;
background-size: 16px 12px !important;
outline: none;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
transition: all 0.2s;
}
.dataTables_wrapper .dataTables_length select:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #cbd5e1;
border-radius: 0.5rem;
padding: 0.4rem 1rem;
margin-left: 0.5rem;
background-color: #f8fafc;
outline: none;
font-family: 'Sarabun', sans-serif;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
transition: all 0.2s;
}
.dataTables_wrapper .dataTables_filter input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
/* Table Styles */
table.dataTable {
width: 100% !important;
border-collapse: collapse !important;
margin-top: 1rem !important;
margin-bottom: 1rem !important;
}
table.dataTable thead th {
border-bottom: 2px solid #e2e8f0 !important;
padding: 1rem;
color: #475569;
text-transform: uppercase;
font-size: 0.85rem;
font-weight: 700;
background-color: #f8fafc;
letter-spacing: 0.5px;
}
table.dataTable tbody td {
border-bottom: 1px solid #f1f5f9;
padding: 1.2rem 1rem;
vertical-align: middle;
}
table.dataTable tbody tr:hover {
background-color: #f8fafc;
}
/* Pagination */
.dataTables_wrapper .dataTables_info {
color: #64748b;
font-size: 0.95rem;
font-weight: 500;
}
.dataTables_wrapper .dataTables_paginate {
padding-top: 1rem;
float: right;
display: flex;
gap: 0.25rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid transparent;
cursor: pointer;
color: #475569;
font-weight: 600;
transition: all 0.2s;
margin-left: 4px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background-color: #f1f5f9;
color: #2563eb;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: linear-gradient(to bottom right, #2563eb, #1d4ed8);
color: #ffffff !important;
box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.3);
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Tooltip Native CSS */
[data-tooltip] {
position: relative;
cursor: help;
}
[data-tooltip]:hover::before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-8px);
background-color: #0f172a;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.85rem;
white-space: nowrap;
z-index: 50;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
[data-tooltip]:hover::after {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(0);
border-width: 6px;
border-style: solid;
border-color: #0f172a transparent transparent transparent;
}
</style>
</head>
<body class="text-slate-800 antialiased">
<div class="bg-gradient-to-r from-white to-slate-50 border-b border-slate-200 py-5 mb-8 shadow-[0_4px_20px_-2px_rgba(0,0,0,0.05)]">
<div class="max-w-[1400px] mx-auto px-4 flex flex-wrap justify-between items-center gap-4">
<h3 class="text-2xl font-black m-0 flex items-center text-gradient tracking-tight">
<i class="fa-solid fa-shield-halved mr-3 text-blue-600"></i> Enterprise Asset Management
</h3>
<div class="flex items-center gap-3">
<span class="inline-flex items-center px-4 py-2 rounded-xl text-sm font-bold bg-slate-100 text-slate-700 border border-slate-200 shadow-inner">
<i class="fa-solid fa-desktop mr-2 text-slate-500"></i> Total Assets: <?= count($agents) ?>
</span>
<button onclick="exportToExcel()" class="inline-flex items-center px-5 py-2 text-sm font-bold rounded-xl text-white bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-600 hover:to-green-700 transition-all shadow-lg shadow-emerald-500/30 transform hover:-translate-y-0.5">
<i class="fa-solid fa-file-excel mr-2"></i> Export Excel
</button>
<a href="?refresh=1" class="inline-flex items-center px-5 py-2 text-sm font-bold rounded-xl text-slate-700 bg-white border border-slate-300 hover:bg-slate-50 transition-all shadow-sm hover:shadow-md transform hover:-translate-y-0.5">
<i class="fa-solid fa-rotate-right mr-2 text-primary"></i> Refresh
</a>
</div>
</div>
</div>
<div class="max-w-[1400px] mx-auto px-4 mb-16">
<?php if (!$token): ?>
<div class="mb-6 bg-gradient-to-r from-red-50 to-red-100 border border-red-200 p-5 rounded-2xl flex items-center shadow-sm">
<i class="fa-solid fa-triangle-exclamation text-3xl text-red-600 mr-5"></i>
<div><strong class="text-red-800 text-lg">Wazuh API Error:</strong> <span class="text-red-700 font-medium">ไม่สามารถยืนยันตัวตนได้ กรุณาตรวจสอบ IP หรือ Credentials</span></div>
</div>
<?php endif; ?>
<?php if ($token && $indexer_status === 'AUTH_FAILED'): ?>
<div class="mb-6 bg-gradient-to-r from-amber-50 to-amber-100 border border-amber-200 p-5 rounded-2xl flex items-center shadow-sm">
<i class="fa-solid fa-lock text-3xl text-warning mr-5"></i>
<div><strong class="text-amber-800 text-lg">Wazuh Indexer Warning:</strong> <br><span class="text-amber-700 font-medium">รหัสผ่าน Indexer ผิดพลาด ระบบจะไม่สามารถแสดงข้อมูล Custom Logs (เช่น PC Model, Remote ID)</span></div>
</div>
<?php elseif ($token && $indexer_status === 'CONNECTION_ERROR'): ?>
<div class="mb-6 bg-gradient-to-r from-amber-50 to-amber-100 border border-amber-200 p-5 rounded-2xl flex items-center shadow-sm">
<i class="fa-solid fa-plug-circle-xmark text-3xl text-warning mr-5"></i>
<div><strong class="text-amber-800 text-lg">Wazuh Indexer Disconnected:</strong> <br><span class="text-amber-700 font-medium">ไม่สามารถเชื่อมต่อ Indexer พอร์ต 9200 ได้</span></div>
</div>
<?php endif; ?>
<?php if (empty($agents) && $token): ?>
<div class="mb-6 bg-blue-50 border border-blue-200 p-5 rounded-2xl flex items-center text-primary font-bold shadow-sm">
<i class="fa-solid fa-circle-info text-2xl mr-4"></i> Connected to API successfully, but no agents found.
</div>
<?php elseif ($token): ?>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="glass-card rounded-2xl p-6">
<h6 class="font-black text-slate-800 text-base mb-5 flex items-center pb-3 border-b border-slate-100 uppercase tracking-wide">
<span class="w-1.5 h-5 rounded-full bg-gradient-to-b from-blue-500 to-blue-700 mr-3 shadow-sm"></span> Operating Systems
</h6>
<div class="relative h-[250px] w-full flex justify-center"><canvas id="osChart"></canvas></div>
</div>
<div class="glass-card rounded-2xl p-6">
<h6 class="font-black text-slate-800 text-base mb-5 flex items-center pb-3 border-b border-slate-100 uppercase tracking-wide">
<span class="w-1.5 h-5 rounded-full bg-gradient-to-b from-sky-400 to-sky-600 mr-3 shadow-sm"></span> Hardware Models
</h6>
<div class="relative h-[250px] w-full flex justify-center"><canvas id="hwChart"></canvas></div>
</div>
<div class="glass-card rounded-2xl p-6">
<h6 class="font-black text-slate-800 text-base mb-5 flex items-center pb-3 border-b border-slate-100 uppercase tracking-wide">
<span class="w-1.5 h-5 rounded-full bg-gradient-to-b from-emerald-400 to-emerald-600 mr-3 shadow-sm"></span> RAM Distribution
</h6>
<div class="relative h-[250px] w-full flex justify-center items-center" id="ramChartContainer">
<div class="text-center text-slate-400">
<div class="inline-flex justify-center items-center w-20 h-20 rounded-full bg-slate-50 border border-slate-100 mb-3 shadow-inner">
<i class="fa-solid fa-chart-pie text-4xl text-slate-300"></i>
</div><br>
<span class="text-sm font-bold tracking-wide">REQUIRES SCAN</span>
</div>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl p-8 mb-8 shadow-xl shadow-slate-900/20 border border-slate-700 relative overflow-hidden group">
<div class="absolute top-0 right-0 -mt-10 -mr-10 w-64 h-64 bg-blue-500 opacity-20 rounded-full blur-3xl group-hover:bg-blue-400 transition-colors duration-700"></div>
<div class="absolute bottom-0 left-10 -mb-10 w-32 h-32 bg-emerald-500 opacity-10 rounded-full blur-2xl"></div>
<div class="flex flex-col md:flex-row items-center justify-between relative z-10">
<div class="mb-5 md:mb-0 text-center md:text-left">
<h4 class="text-white font-black text-2xl mb-2 tracking-wide flex items-center justify-center md:justify-start">
<i class="fa-solid fa-radar text-sky-400 mr-3 drop-shadow-[0_0_8px_rgba(56,189,248,0.5)]"></i> GLOBAL DEEP SCAN
</h4>
<p class="text-slate-300 text-sm m-0 font-medium">Run a comprehensive scan to fetch Live RAM, Software, and Updates across all endpoints.</p>
</div>
<button id="btnScanGlobal" class="px-8 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-blue-600/30 transition-all transform hover:-translate-y-1" onclick="startGlobalScan()">
<i class="fa-solid fa-magnifying-glass mr-2"></i> Start Scan
</button>
</div>
<div id="scanProgressArea" class="mt-6 hidden relative z-10">
<div class="flex justify-between mb-2">
<span class="text-sm font-bold text-slate-300 uppercase tracking-wider" id="scanStatusText">Scanning Endpoints...</span>
<span class="text-sm font-black text-sky-400" id="scanPercentage">0%</span>
</div>
<div class="w-full bg-slate-950 rounded-full h-3 shadow-inner overflow-hidden border border-slate-700">
<div id="scanProgressBar" class="bg-gradient-to-r from-blue-500 to-sky-400 h-full rounded-full transition-all duration-300"></div>
</div>
</div>
</div>
<div class="glass-card rounded-2xl overflow-hidden mb-10">
<div class="flex overflow-x-auto hide-scrollbar bg-slate-50 p-2 gap-2 border-b border-slate-200">
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-primary bg-white shadow-sm border border-slate-200 whitespace-nowrap outline-none transition-all" data-target="#tab-inventory">
<i class="fa-solid fa-list text-primary mr-1.5"></i> Agent Inventory
</button>
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-asset-filter">
<i class="fa-solid fa-filter text-slate-400 mr-1.5"></i> Asset Filter
</button>
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-global-software">
<i class="fa-solid fa-box-open text-slate-400 mr-1.5"></i> Global Software
</button>
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-global-updates">
<i class="fa-brands fa-windows text-slate-400 mr-1.5"></i> Global Update
</button>
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-global-process">
<i class="fa-solid fa-terminal text-slate-400 mr-1.5"></i> Check Process
</button>
</div>
<div class="p-6">
<div id="tab-inventory" class="tab-pane block">
<div class="overflow-x-auto pb-4">
<table id="mainAgentTable" class="w-full text-left">
<thead>
<tr>
<th class="w-16 text-center">OS</th>
<th>Endpoint & Asset Info</th>
<th>IP Address</th>
<th>Version</th>
<th>Status</th>
<th class="text-center">Remote</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="text-slate-700 font-medium">
<?php foreach ($agents as $agent):
$os_platform = strtolower($agent['os']['platform'] ?? '');
if ($agent['id'] === '000') $os_platform = 'wazuh server';
$icon = 'fa-solid fa-desktop text-slate-300';
if (strpos($os_platform, 'windows') !== false) $icon = 'fa-brands fa-windows text-blue-500';
elseif (strpos($os_platform, 'ubuntu') !== false || strpos($os_platform, 'linux') !== false || strpos($os_platform, 'centos') !== false) $icon = 'fa-brands fa-linux text-orange-500';
elseif (strpos($os_platform, 'darwin') !== false || strpos($os_platform, 'mac') !== false) $icon = 'fa-brands fa-apple text-slate-600';
elseif (strpos($os_platform, 'server') !== false) $icon = 'fa-solid fa-server text-emerald-500';
$status = strtolower($agent['status'] ?? 'unknown');
$status_bg = $status === 'active' ? 'bg-green-100 text-success border border-green-200' : ($status === 'disconnected' ? 'bg-red-100 text-danger border border-red-200' : 'bg-slate-100 text-slate-600 border border-slate-200');
$status_icon = $status === 'active' ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-xmark';
$aid = $agent['id'];
$tv_id = (isset($all_metadata[$aid]['tv']) && $all_metadata[$aid]['tv'] !== 'None') ? htmlspecialchars($all_metadata[$aid]['tv']) : null;
$ad_id = (isset($all_metadata[$aid]['ad']) && $all_metadata[$aid]['ad'] !== 'None') ? htmlspecialchars($all_metadata[$aid]['ad']) : null;
$pc_model = (isset($all_metadata[$aid]['model']) && $all_metadata[$aid]['model'] !== 'Unknown') ? htmlspecialchars($all_metadata[$aid]['model']) : null;
$pc_manufacturer = (isset($all_metadata[$aid]['manufacturer']) && $all_metadata[$aid]['manufacturer'] !== 'Unknown') ? htmlspecialchars($all_metadata[$aid]['manufacturer']) : null;
$asset_id = isset($all_metadata[$aid]['asset_id']) ? htmlspecialchars($all_metadata[$aid]['asset_id']) : null;
$location = isset($all_metadata[$aid]['location']) ? htmlspecialchars($all_metadata[$aid]['location']) : null;
?>
<tr class="group">
<td data-sort="<?= $os_platform ?>" class="text-center"><i class="<?= $icon ?> text-3xl group-hover:scale-110 transition-transform"></i></td>
<td>
<div class="font-black text-slate-900 text-base"><?= htmlspecialchars($agent['name']) ?></div>
<?php $machine_info = trim("$pc_manufacturer $pc_model"); ?>
<?php if ($machine_info): ?>
<div class="text-xs font-semibold text-slate-500 mt-1"><i class="fa-solid fa-laptop mr-1 text-slate-400"></i><?= $machine_info ?></div>
<?php endif; ?>
<div class="mt-2.5 flex flex-wrap gap-2">
<?php if ($asset_id): ?><span class="inline-flex items-center px-2 py-0.5 rounded border border-slate-200 bg-white shadow-sm text-[11px] font-bold text-slate-600"><i class="fa-solid fa-tag mr-1.5 text-blue-500"></i><?= $asset_id ?></span><?php endif; ?>
<?php if ($location): ?><span class="inline-flex items-center px-2 py-0.5 rounded border border-slate-200 bg-white shadow-sm text-[11px] font-bold text-slate-600"><i class="fa-solid fa-location-dot mr-1.5 text-emerald-500"></i><?= $location ?></span><?php endif; ?>
</div>
</td>
<td><code class="text-red-600 bg-red-50 border border-red-100 px-2.5 py-1 rounded-md text-sm shadow-inner"><?= htmlspecialchars($agent['ip'] ?? 'N/A') ?></code></td>
<td class="font-bold text-slate-700"><?= htmlspecialchars(format_os_name($agent)) ?></td>
<td>
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold shadow-sm <?= $status_bg ?>">
<i class="<?= $status_icon ?> mr-1.5"></i> <?= ucfirst($status) ?>
</span>
</td>
<td class="text-center text-nowrap">
<?php
if ($tv_id || $ad_id) {
if ($tv_id) echo "<span class='inline-flex justify-center items-center w-10 h-10 rounded-xl bg-blue-50 text-blue-600 border border-blue-100 cursor-help hover:bg-blue-600 hover:text-white transition-all shadow-sm' data-tooltip='TeamViewer: {$tv_id}'><i class='fa-solid fa-people-arrows text-lg'></i></span>";
if ($ad_id) echo "<span class='inline-flex justify-center items-center w-10 h-10 rounded-xl bg-red-50 text-red-600 border border-red-100 cursor-help hover:bg-red-600 hover:text-white transition-all shadow-sm ml-2' data-tooltip='AnyDesk: {$ad_id}'><i class='fa-solid fa-desktop text-lg'></i></span>";
} else {
echo "<span class='text-slate-300 text-sm font-bold'>-</span>";
}
?>
</td>
<td class="text-right">
<button class="inline-flex items-center px-4 py-2 text-sm font-bold rounded-xl text-blue-700 bg-blue-50 border border-blue-200 hover:bg-blue-600 hover:text-white hover:border-blue-600 transition-all shadow-sm transform hover:-translate-y-0.5" onclick="showDetailedModal('<?= $agent['id'] ?>')">
<i class="fa-solid fa-microchip mr-2"></i> Specs
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div id="tab-asset-filter" class="tab-pane hidden">
<div id="filterNotScannedMsg" class="bg-blue-50 border border-blue-200 text-blue-700 p-5 rounded-2xl flex items-center mb-0 font-bold shadow-sm">
<i class="fa-solid fa-info-circle text-2xl mr-4 text-blue-500"></i> Please run "Global Deep Scan" above to fetch RAM data.
</div>
<div id="filterContentArea" style="display:none;">
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 p-6 bg-slate-50 border border-slate-200 rounded-2xl mb-8 shadow-inner">
<div>
<label class="block text-sm font-black text-slate-700 mb-2 uppercase tracking-wide">OS Platform</label>
<select id="filterOS" class="block w-full bg-white border border-slate-300 text-slate-700 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 p-3 outline-none font-sans appearance-none shadow-sm font-medium" style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%23475569\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'m2 5 6 6 6-6\'/%3e%3c/svg%3e'); background-repeat: no-repeat; background-position: right 1rem center; background-size: 16px 12px;">
<option value="all">All OS</option>
<?php foreach ($unique_os_list as $os_opt): ?>
<option value="<?= htmlspecialchars($os_opt) ?>"><?= htmlspecialchars($os_opt) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-sm font-black text-slate-700 mb-2 uppercase tracking-wide">RAM Capacity</label>
<select id="filterRAM" class="block w-full bg-white border border-slate-300 text-slate-700 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 p-3 outline-none font-sans appearance-none shadow-sm font-medium" style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%23475569\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'m2 5 6 6 6-6\'/%3e%3c/svg%3e'); background-repeat: no-repeat; background-position: right 1rem center; background-size: 16px 12px;">
<option value="all">All RAM Sizes</option>
<option value="<4">Less than 4 GB</option>
<option value="4-8">4 GB - 8 GB</option>
<option value="8-16">8 GB - 16 GB</option>
<option value=">16">More than 16 GB</option>
</select>
</div>
<div class="flex items-end">
<button class="w-full inline-flex items-center justify-center px-4 py-3 text-sm font-bold rounded-xl text-white bg-slate-800 hover:bg-slate-900 transition-all shadow-md transform hover:-translate-y-0.5" onclick="applyAssetFilter()">
<i class="fa-solid fa-filter mr-2"></i> Apply Filter
</button>
</div>
</div>
<div class="overflow-x-auto pb-4">
<table id="assetFilterTable" class="w-full text-left">
<thead>
<tr>
<th>Agent Name</th>
<th>IP Address</th>
<th>OS Version</th>
<th>Total RAM</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody class="text-slate-700 font-medium"></tbody>
</table>
</div>
</div>
</div>
<div id="tab-global-software" class="tab-pane hidden">
<div id="softwareNotScannedMsg" class="bg-blue-50 border border-blue-200 text-blue-700 p-5 rounded-2xl flex items-center mb-0 font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4 text-blue-500"></i> Please run "Global Deep Scan" above.</div>
<div id="softwareTableContainer" style="display:none;" class="overflow-x-auto pb-4">
<table id="globalSoftwareTable" class="w-full text-left">
<thead>
<tr>
<th class="text-left">Software Name</th>
<th>Version</th>
<th>Vendor</th>
<th>Installed On (Agent)</th>
</tr>
</thead>
<tbody class="text-slate-700 font-medium"></tbody>
</table>
</div>
</div>
<div id="tab-global-updates" class="tab-pane hidden">
<div id="updatesNotScannedMsg" class="bg-blue-50 border border-blue-200 text-blue-700 p-5 rounded-2xl flex items-center mb-0 font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4 text-blue-500"></i> Please run "Global Deep Scan" above.</div>
<div id="updatesTableContainer" style="display:none;" class="overflow-x-auto pb-4">
<table id="globalUpdateTable" class="w-full text-left">
<thead>
<tr>
<th class="text-left w-1/4">Hotfix ID</th>
<th>Description / Subject</th>
<th>Installed On (Agent)</th>
</tr>
</thead>
<tbody class="text-slate-700 font-medium"></tbody>
</table>
</div>
</div>
<div id="tab-global-process" class="tab-pane hidden">
<div class="max-w-3xl mx-auto mb-8">
<div class="relative flex items-center w-full h-14 rounded-2xl focus-within:ring-4 focus-within:ring-blue-500/20 bg-white overflow-hidden border border-slate-300 shadow-sm transition-all">
<div class="grid place-items-center h-full w-14 text-slate-400 text-lg"><i class="fa-solid fa-magnifying-glass"></i></div>
<input class="peer h-full w-full outline-none text-base text-slate-700 font-bold pr-2 bg-transparent" type="text" id="inputProcessSearch" placeholder="Enter process name (e.g. chrome, powershell)..." />
<button class="h-full px-8 bg-slate-800 hover:bg-slate-900 text-white font-bold text-sm transition-colors" type="button" id="btnSearchProcess" onclick="searchGlobalProcess()">Search</button>
</div>
</div>
<div id="processProgressArea" class="max-w-3xl mx-auto mb-8 hidden">
<div class="flex justify-between mb-2"><small class="font-bold text-slate-600 uppercase tracking-wide" id="processStatusText">Checking Agent...</small><small class="font-black text-blue-600" id="processPercentage">0%</small></div>
<div class="w-full bg-slate-200 rounded-full h-3 shadow-inner">
<div id="processProgressBar" class="bg-blue-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<div id="processTableContainer" style="display:none;" class="overflow-x-auto pb-4">
<table id="globalProcessTable" class="w-full text-left">
<thead>
<tr>
<th class="text-left">Process Name</th>
<th>PID</th>
<th>State</th>
<th>Command / Path</th>
<th>Running On (Agent)</th>
</tr>
</thead>
<tbody class="text-slate-700 font-medium"></tbody>
</table>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
<div id="agentModal" class="fixed inset-0 z-[1050] hidden bg-slate-900/70 backdrop-blur-md overflow-y-auto flex items-start justify-center pt-8 pb-10 px-4 transition-opacity">
<div class="bg-white rounded-3xl shadow-2xl w-full max-w-6xl flex flex-col max-h-[92vh] border border-slate-200 overflow-hidden">
<div class="flex items-center justify-between p-6 border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white">
<h5 class="text-2xl font-black text-slate-800 m-0 flex items-center" id="modalTitle"></h5>
<button type="button" class="text-slate-400 bg-white border border-slate-200 hover:bg-red-50 hover:text-red-600 hover:border-red-200 rounded-xl text-sm w-10 h-10 flex justify-center items-center transition-all shadow-sm" onclick="closeModal()">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto p-0 bg-slate-50/50">
<div class="flex border-b border-slate-200 overflow-x-auto hide-scrollbar bg-white px-6 pt-4 gap-2">
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-primary border-b-[3px] border-primary focus:outline-none whitespace-nowrap transition-colors rounded-t-lg bg-blue-50/50" data-target="#general">
<i class="fa-solid fa-circle-info mr-2"></i> General
</button>
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#hardware">
<i class="fa-solid fa-server mr-2"></i> Hardware & Disk
</button>
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#network">
<i class="fa-solid fa-network-wired mr-2"></i> Network
</button>
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#software">
<i class="fa-solid fa-box-open mr-2"></i> Software
</button>
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#hotfixes">
<i class="fa-brands fa-windows mr-2"></i> Windows Update
</button>
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#remote-desktop">
<i class="fa-solid fa-headset mr-2"></i> Remote
</button>
</div>
<div class="p-6 md:p-8">
<div id="general" class="modal-tab-pane block">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6" id="content-general"></div>
</div>
<div id="hardware" class="modal-tab-pane hidden">
<div id="content-hardware" class="text-center py-10"></div>
</div>
<div id="network" class="modal-tab-pane hidden">
<div id="content-network" class="text-center py-10"></div>
</div>
<div id="software" class="modal-tab-pane hidden">
<div id="content-software" class="text-center py-10"></div>
</div>
<div id="hotfixes" class="modal-tab-pane hidden">
<div id="content-hotfixes" class="text-center py-10"></div>
</div>
<div id="remote-desktop" class="modal-tab-pane hidden">
<div id="content-remote-desktop" class="text-center py-10"></div>
</div>
</div>
</div>
<div class="flex items-center justify-end p-5 border-t border-slate-200 bg-white">
<button type="button" class="px-8 py-3 text-sm font-bold rounded-xl text-slate-700 bg-white border border-slate-300 hover:bg-slate-100 transition-all shadow-sm" onclick="closeModal()">Close Window</button>
</div>
</div>
</div>
<script>
const agentsData = <?= json_encode($agents) ?>;
const allMetadata = <?= json_encode($all_metadata) ?>;
document.querySelectorAll('.tab-btn').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('text-primary', 'border-primary', 'bg-white', 'shadow-sm');
btn.classList.add('text-slate-500', 'border-transparent');
});
button.classList.remove('text-slate-500', 'border-transparent');
button.classList.add('text-primary', 'border-primary', 'bg-white', 'shadow-sm');
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.add('hidden'));
document.querySelector(button.dataset.target).classList.remove('hidden');
});
});
document.querySelectorAll('.modal-tab-btn').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll('.modal-tab-btn').forEach(btn => {
btn.classList.remove('text-primary', 'border-primary', 'bg-blue-50/50');
btn.classList.add('text-slate-500', 'border-transparent');
});
button.classList.remove('text-slate-500', 'border-transparent');
button.classList.add('text-primary', 'border-primary', 'bg-blue-50/50');
document.querySelectorAll('.modal-tab-pane').forEach(pane => pane.classList.add('hidden'));
document.querySelector(button.dataset.target).classList.remove('hidden');
});
});
function closeModal() {
document.getElementById('agentModal').classList.add('hidden');
document.body.style.overflow = 'auto';
}
const corporateColors = [
'#1e3a8a', '#059669', '#d97706', '#dc2626',
'#475569', '#0284c7', '#7c3aed', '#ea580c'
];
$(document).ready(function() {
$('#mainAgentTable').DataTable({
pageLength: 10,
order: [
[1, 'asc']
],
language: {
search: "_INPUT_",
searchPlaceholder: "Search endpoints..."
}
});
renderOSChart();
renderHWChart();
document.getElementById("inputProcessSearch").addEventListener("keypress", function(e) {
if (e.key === "Enter") {
e.preventDefault();
searchGlobalProcess();
}
});
});
function exportToExcel() {
let wb = XLSX.utils.book_new();
let wsData = [];
wsData.push([
"Agent ID", "Agent Name", "Manufacturer", "PC Model", "Asset ID", "Location",
"IP Address", "OS Name", "OS Version", "Architecture",
"Status", "TeamViewer ID", "AnyDesk Address",
"Disk C: Total (GB)", "Disk C: Free (GB)", "Total RAM",
"Date Added", "Last Keep Alive"
]);
agentsData.forEach(agent => {
let aid = agent.id;
let meta = allMetadata[aid] || {};
let assetId = agent.labels?.asset_id || meta.asset_id || '-';
let location = agent.labels?.location || meta.location || '-';
let pcManufacturer = meta.manufacturer && meta.manufacturer !== 'Unknown' ? meta.manufacturer : '-';
let pcModel = meta.model && meta.model !== 'Unknown' ? meta.model : '-';
let tv = meta.tv && meta.tv !== 'None' ? meta.tv : '-';
let ad = meta.ad && meta.ad !== 'None' ? meta.ad : '-';
let diskTotal = meta.disk_c_total ? meta.disk_c_total : 'N/A';
let diskFree = meta.disk_c_free ? meta.disk_c_free : 'N/A';
let osName = getFormattedOSName(agent);
let osVersion = agent.os?.version || '-';
let arch = agent.os?.arch || '-';
let dateAdd = agent.dateAdd ? new Date(agent.dateAdd).toLocaleDateString() : '-';
let lastAlive = agent.lastKeepAlive ? new Date(agent.lastKeepAlive).toLocaleString() : '-';
let scannedData = globalAgentHardwareList.find(a => a.id === aid);
let ram = scannedData ? scannedData.ramStr : 'Not Scanned';
wsData.push([
aid, agent.name, pcManufacturer, pcModel, assetId, location,
agent.ip || '-', osName, osVersion, arch,
agent.status || '-', tv, ad,
diskTotal, diskFree, ram,
dateAdd, lastAlive
]);
});
let ws = XLSX.utils.aoa_to_sheet(wsData);
XLSX.utils.book_append_sheet(wb, ws, "Inventory");
let d = new Date();
let dateStr = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, '0') + "-" + String(d.getDate()).padStart(2, '0');
XLSX.writeFile(wb, "Endpoint_Inventory_" + dateStr + ".xlsx");
}
function getFormattedOSName(agent) {
if (agent.id === '000') return 'Wazuh Server';
let osName = agent.os?.name || agent.os?.platform || 'Unknown';
let nameLower = osName.toLowerCase();
if (nameLower.includes('windows 11')) return 'Windows 11';
if (nameLower.includes('windows 10')) return 'Windows 10';
if (nameLower.includes('windows 7')) return 'Windows 7';
if (nameLower.includes('windows 8')) return 'Windows 8';
if (nameLower.includes('windows server 2022')) return 'Windows Server 2022';
if (nameLower.includes('windows server 2019')) return 'Windows Server 2019';
if (nameLower.includes('windows server 2016')) return 'Windows Server 2016';
if (nameLower.includes('windows server 2012')) return 'Windows Server 2012';
if (nameLower.includes('ubuntu')) {
let m = osName.match(/Ubuntu (\d+\.\d+)/i);
return m ? `Ubuntu ${m[1]}` : 'Ubuntu';
}
if (nameLower.includes('centos')) {
let m = osName.match(/CentOS.*?(\d+)/i);
return m ? `CentOS ${m[1]}` : 'CentOS';
}
if (nameLower.includes('mac') || nameLower.includes('darwin')) {
return agent.os?.version ? `macOS ${agent.os.version}` : 'macOS';
}
return osName;
}
function renderOSChart() {
let osCounts = {};
agentsData.forEach(a => {
let osName = getFormattedOSName(a);
osCounts[osName] = (osCounts[osName] || 0) + 1;
});
if (Object.keys(osCounts).length > 0) {
new Chart(document.getElementById('osChart').getContext('2d'), {
type: 'doughnut',
data: {
labels: Object.keys(osCounts),
datasets: [{
data: Object.values(osCounts),
backgroundColor: corporateColors,
borderWidth: 3,
borderColor: '#ffffff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
font: {
family: 'Sarabun',
size: 13,
weight: 'bold'
},
boxWidth: 12,
color: '#334155'
}
}
}
}
});
}
}
function renderHWChart() {
let mfgData = {};
let allModels = new Set();
agentsData.forEach(a => {
if (a.id === '000') return;
let meta = allMetadata[a.id] || {};
let mfg = meta.manufacturer && meta.manufacturer !== 'Unknown' ? meta.manufacturer : 'Unknown';
let model = meta.model && meta.model !== 'Unknown' ? meta.model : 'Unknown';
if (mfg.toLowerCase().includes('hewlett-packard') || mfg.toLowerCase() === 'hp') mfg = 'HP';
else if (mfg.toLowerCase().includes('lenovo')) mfg = 'Lenovo';
else if (mfg.toLowerCase().includes('dell')) mfg = 'Dell';
else if (mfg.toLowerCase().includes('vmware')) mfg = 'VMware';
else if (mfg.toLowerCase().includes('microsoft')) mfg = 'Microsoft';
if (!mfgData[mfg]) mfgData[mfg] = {};
mfgData[mfg][model] = (mfgData[mfg][model] || 0) + 1;
allModels.add(model);
});
let labels = Object.keys(mfgData);
if (labels.length === 0) return;
let datasets = [];
let colorIdx = 0;
allModels.forEach(model => {
let data = [];
let hasData = false;
labels.forEach(mfg => {
let count = mfgData[mfg][model] || 0;
data.push(count);
if (count > 0) hasData = true;
});
if (hasData) {
datasets.push({
label: model,
data: data,
backgroundColor: corporateColors[colorIdx % corporateColors.length],
borderRadius: 4
});
colorIdx++;
}
});
new Chart(document.getElementById('hwChart').getContext('2d'), {
type: 'bar',
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
grid: {
display: false
},
ticks: {
font: {
family: 'Sarabun',
weight: 'bold'
},
color: '#475569'
}
},
y: {
stacked: true,
beginAtZero: true,
border: {
display: false
},
grid: {
color: '#f1f5f9'
},
ticks: {
stepSize: 1,
font: {
family: 'Sarabun',
weight: 'bold'
},
color: '#475569'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
titleFont: {
family: 'Sarabun',
size: 14,
weight: 'bold'
},
bodyFont: {
family: 'Sarabun',
size: 13
},
callbacks: {
title: function(context) {
return 'Brand: ' + context[0].label;
},
label: function(context) {
return ' ' + context.dataset.label + ': ' + context.parsed.y;
}
}
}
}
}
});
}
let globalAgentHardwareList = [];
let globalSoftwareList = [];
let globalUpdateList = [];
let globalRamCounts = {
"< 4GB": 0,
"4 - 8GB": 0,
"8 - 16GB": 0,
"> 16GB": 0
};
async function startGlobalScan() {
const scanTargets = agentsData.filter(a => a.status === 'active' || a.status === 'disconnected');
if (scanTargets.length === 0) return alert("No active agents found to scan.");
let btn = document.getElementById('btnScanGlobal');
btn.disabled = true;
btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i> Scanning...`;
document.getElementById('scanProgressArea').classList.remove('hidden');
let completed = 0;
globalAgentHardwareList = [];
globalSoftwareList = [];
globalUpdateList = [];
globalRamCounts = {
"< 4GB": 0,
"4 - 8GB": 0,
"8 - 16GB": 0,
"> 16GB": 0
};
for (let agent of scanTargets) {
let ramBytes = 0;
let ramStr = 'N/A';
try {
let res = await fetch(`?action=get_dashboard_data&agent_id=${agent.id}`);
let data = await res.json();
if (data.hardware && data.hardware.ram && data.hardware.ram.total) {
ramBytes = data.hardware.ram.total;
ramStr = formatKB(ramBytes);
let gb = ramBytes / 1024 / 1024;
if (gb < 4.0) globalRamCounts["< 4GB"]++;
else if (gb <= 8.0) globalRamCounts["4 - 8GB"]++;
else if (gb <= 16.0) globalRamCounts["8 - 16GB"]++;
else globalRamCounts["> 16GB"]++;
}
if (data.packages) {
data.packages.forEach(pkg => {
globalSoftwareList.push([pkg.name || '-', pkg.version || '-', pkg.vendor || '-', `<div class="font-bold text-slate-900">${agent.name}</div><div class="text-xs text-slate-500 font-mono mt-1">${agent.ip || '-'}</div>`]);
});
}
if (data.hotfixes) {
data.hotfixes.forEach(hf => {
globalUpdateList.push([`<span class="text-success font-bold"><i class="fa-solid fa-shield-virus mr-2"></i>${hf.hotfix || '-'}</span>`, hf.description || hf.caption || 'System Update', `<div class="font-bold text-slate-900">${agent.name}</div><div class="text-xs text-slate-500 font-mono mt-1">${agent.ip || '-'}</div>`]);
});
}
} catch (err) {}
globalAgentHardwareList.push({
id: agent.id,
name: agent.name,
ip: agent.ip || 'N/A',
osName: getFormattedOSName(agent),
ramBytes: ramBytes,
ramStr: ramStr
});
completed++;
let percent = Math.round((completed / scanTargets.length) * 100);
document.getElementById('scanStatusText').innerText = `Scanning Agent ${completed} of ${scanTargets.length}...`;
document.getElementById('scanPercentage').innerText = `${percent}%`;
document.getElementById('scanProgressBar').style.width = `${percent}%`;
}
btn.innerHTML = `<i class="fa-solid fa-check mr-2"></i> Scan Complete!`;
btn.classList.replace('from-blue-600', 'from-emerald-500');
btn.classList.replace('to-indigo-600', 'to-green-600');
btn.classList.replace('shadow-blue-600/30', 'shadow-emerald-500/30');
document.getElementById('ramChartContainer').innerHTML = '<canvas id="ramChart"></canvas>';
new Chart(document.getElementById('ramChart').getContext('2d'), {
type: 'pie',
data: {
labels: Object.keys(globalRamCounts),
datasets: [{
data: Object.values(globalRamCounts),
backgroundColor: corporateColors,
borderWidth: 3,
borderColor: '#ffffff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
font: {
family: 'Sarabun',
weight: 'bold'
},
color: '#334155'
}
}
}
}
});
document.getElementById('filterNotScannedMsg').style.display = 'none';
document.getElementById('filterContentArea').style.display = 'block';
applyAssetFilter();
document.getElementById('softwareNotScannedMsg').style.display = 'none';
document.getElementById('softwareTableContainer').style.display = 'block';
$('#globalSoftwareTable').DataTable({
data: globalSoftwareList,
pageLength: 10,
deferRender: true,
order: [
[0, 'asc']
],
language: {
search: "_INPUT_",
searchPlaceholder: "Filter software..."
},
destroy: true
});
document.getElementById('updatesNotScannedMsg').style.display = 'none';
document.getElementById('updatesTableContainer').style.display = 'block';
$('#globalUpdateTable').DataTable({
data: globalUpdateList,
pageLength: 10,
deferRender: true,
order: [
[0, 'asc']
],
language: {
search: "_INPUT_",
searchPlaceholder: "Filter Update (e.g. KB500)..."
},
destroy: true
});
}
function applyAssetFilter() {
const osFilter = document.getElementById('filterOS').value;
const ramFilter = document.getElementById('filterRAM').value;
let filteredData = globalAgentHardwareList.filter(agent => {
let osMatch = true;
if (osFilter !== 'all') {
osMatch = (agent.osName === osFilter);
}
let ramMatch = true;
if (ramFilter !== 'all') {
let gb = agent.ramBytes / 1024 / 1024;
if (ramFilter === '<4') ramMatch = gb > 0 && gb < 4.0;
else if (ramFilter === '4-8') ramMatch = gb >= 4.0 && gb <= 8.0;
else if (ramFilter === '8-16') ramMatch = gb > 8.0 && gb <= 16.0;
else if (ramFilter === '>16') ramMatch = gb > 16.0;
}
return osMatch && ramMatch;
});
let tableData = filteredData.map(a => [
`<div class="font-black text-slate-900">${a.name}</div>`,
`<code class="text-danger bg-red-50 border border-red-100 px-2.5 py-1 rounded-md text-sm shadow-inner">${a.ip}</code>`,
`<span class="font-bold text-slate-700">${a.osName}</span>`,
`<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold bg-green-100 text-success border border-green-200"><i class="fa-solid fa-memory mr-1.5"></i> ${a.ramStr}</span>`,
`<button class="inline-flex items-center px-4 py-2 text-sm font-bold rounded-xl text-primary bg-blue-50 border border-blue-200 hover:bg-blue-600 hover:text-white transition-all shadow-sm transform hover:-translate-y-0.5" onclick="showDetailedModal('${a.id}')"><i class="fa-solid fa-microchip mr-2"></i> Specs</button>`
]);
if ($.fn.DataTable.isDataTable('#assetFilterTable')) {
$('#assetFilterTable').DataTable().destroy();
}
$('#assetFilterTable').DataTable({
data: tableData,
pageLength: 10,
deferRender: true,
language: {
search: "_INPUT_",
searchPlaceholder: "Search filtered results..."
},
columnDefs: [{
targets: 4,
className: "text-center",
orderable: false
}]
});
}
let globalProcessList = [];
async function searchGlobalProcess() {
const query = document.getElementById('inputProcessSearch').value.trim().toLowerCase();
if (!query) return alert("Please enter a process name.");
const scanTargets = agentsData.filter(a => a.status === 'active');
if (scanTargets.length === 0) return alert("No active agents.");
let btn = document.getElementById('btnSearchProcess');
btn.disabled = true;
btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i> Checking...`;
document.getElementById('processProgressArea').classList.remove('hidden');
document.getElementById('processTableContainer').style.display = 'none';
globalProcessList = [];
let completed = 0;
for (let agent of scanTargets) {
try {
let res = await fetch(`?action=get_processes&agent_id=${agent.id}`);
let data = await res.json();
if (data.processes) {
data.processes.forEach(proc => {
let procName = (proc.name || '').toLowerCase();
let cmdLine = (proc.cmd || proc.cmdline || '').toLowerCase();
if (procName.includes(query) || cmdLine.includes(query)) {
let stateClass = proc.state === 'running' || proc.state === 'R' ? 'bg-green-100 text-success border border-green-200' : (proc.state === 'sleeping' || proc.state === 'S' ? 'bg-slate-100 text-slate-600 border border-slate-200' : 'bg-amber-100 text-warning border border-amber-200');
globalProcessList.push([
`<span class="font-bold text-slate-900">${proc.name || '-'}</span>`,
`<code class="text-slate-600 bg-slate-50 border border-slate-200 px-2 py-0.5 rounded shadow-inner">${proc.pid || '-'}</code>`,
`<span class="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-bold ${stateClass}">${proc.state || 'Unknown'}</span>`,
`<div class="text-xs text-slate-500 break-all max-w-md bg-slate-50 p-2 rounded border border-slate-100">${proc.cmd || proc.cmdline || '-'}</div>`,
`<div class="font-black text-slate-900">${agent.name}</div><div class="text-xs text-slate-500 font-mono mt-1">${agent.ip || '-'}</div>`
]);
}
});
}
} catch (err) {}
completed++;
let percent = Math.round((completed / scanTargets.length) * 100);
document.getElementById('processStatusText').innerText = `Checking Agent ${completed} of ${scanTargets.length}...`;
document.getElementById('processPercentage').innerText = `${percent}%`;
document.getElementById('processProgressBar').style.width = `${percent}%`;
}
btn.disabled = false;
btn.innerHTML = `Search`;
document.getElementById('processProgressArea').classList.add('hidden');
document.getElementById('processTableContainer').style.display = 'block';
if ($.fn.DataTable.isDataTable('#globalProcessTable')) {
$('#globalProcessTable').DataTable().destroy();
}
$('#globalProcessTable').DataTable({
data: globalProcessList,
pageLength: 10,
deferRender: true,
order: [
[4, 'asc'],
[0, 'asc']
],
language: {
search: "_INPUT_",
searchPlaceholder: "Filter within results..."
}
});
}
function formatKB(kb) {
if (!kb || isNaN(kb)) return 'N/A';
if (kb >= 1048576) return (kb / 1024 / 1024).toFixed(2) + ' GB';
return (kb / 1024).toFixed(2) + ' MB';
}
function showDetailedModal(agentId) {
const agent = agentsData.find(a => a.id === agentId);
if (!agent) return;
let meta = allMetadata[agentId] || {};
let assetId = agent.labels?.asset_id || meta.asset_id || 'ไม่ระบุ';
let locationStr = agent.labels?.location || meta.location || 'ไม่ระบุ';
let pcModel = meta.model && meta.model !== 'Unknown' ? meta.model : 'ไม่ระบุ';
let pcManufacturer = meta.manufacturer && meta.manufacturer !== 'Unknown' ? meta.manufacturer : 'ไม่ระบุ';
document.getElementById('modalTitle').innerHTML = `<i class="fa-solid fa-terminal mr-3 text-primary"></i> ${agent.name} <span class="bg-slate-100 text-slate-600 border border-slate-300 ml-3 px-3 py-1 rounded-lg text-sm font-bold shadow-inner">ID: ${agent.id}</span>`;
// Reset Internal Tabs to General
document.querySelectorAll('.modal-tab-btn').forEach(btn => {
btn.classList.remove('text-primary', 'border-primary', 'bg-blue-50/50');
btn.classList.add('text-slate-500', 'border-transparent');
});
document.querySelector('.modal-tab-btn[data-target="#general"]').classList.remove('text-slate-500', 'border-transparent');
document.querySelector('.modal-tab-btn[data-target="#general"]').classList.add('text-primary', 'border-primary', 'bg-blue-50/50');
document.querySelectorAll('.modal-tab-pane').forEach(pane => pane.classList.add('hidden'));
document.getElementById('general').classList.remove('hidden');
document.getElementById('content-general').innerHTML = `
<div class="col-span-1 md:col-span-2">
<div class="bg-white border border-slate-200 border-l-4 border-l-warning rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
<h6 class="text-warning font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-building mr-2"></i> Asset Information</h6>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Manufacturer</p><p class="text-slate-900 font-black text-lg m-0">${pcManufacturer}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Model</p><p class="text-slate-900 font-black text-lg m-0">${pcModel}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Asset ID</p><p class="text-primary font-black text-lg m-0">${assetId}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Location</p><p class="text-success font-black text-lg m-0">${locationStr}</p></div>
</div>
</div>
</div>
<div class="col-span-1">
<div class="bg-white border border-slate-200 border-l-4 border-l-primary rounded-xl p-6 shadow-sm h-full hover:shadow-md transition-all">
<h6 class="text-primary font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-compact-disc mr-2"></i> OS Info</h6>
<div class="grid grid-cols-2 gap-y-5 gap-x-4">
<div class="col-span-2"><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Name</p><p class="text-slate-900 font-black text-lg m-0">${getFormattedOSName(agent)}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Arch</p><p class="text-slate-900 font-bold m-0">${agent.os?.arch || '-'}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Version</p><p class="text-slate-900 font-bold m-0">${agent.os?.version || '-'}</p></div>
</div>
</div>
</div>
<div class="col-span-1">
<div class="bg-white border border-slate-200 border-l-4 border-l-success rounded-xl p-6 shadow-sm h-full hover:shadow-md transition-all">
<h6 class="text-success font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-heart-pulse mr-2"></i> Agent Status</h6>
<div class="grid grid-cols-2 gap-y-5 gap-x-4">
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">IP Address</p><p class="text-slate-900 font-bold m-0"><code class="bg-red-50 text-danger border border-red-100 px-2 py-1 rounded shadow-inner">${agent.ip || '-'}</code></p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Version</p><p class="text-slate-900 font-bold m-0">${agent.version || '-'}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Added</p><p class="text-slate-900 font-bold m-0">${agent.dateAdd ? new Date(agent.dateAdd).toLocaleDateString() : '-'}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Last Alive</p><p class="text-slate-900 font-bold m-0">${agent.lastKeepAlive ? new Date(agent.lastKeepAlive).toLocaleString() : '-'}</p></div>
</div>
</div>
</div>`;
const loader = `<div class="flex flex-col items-center justify-center py-16"><i class="fa-solid fa-circle-notch fa-spin text-5xl text-primary mb-5 drop-shadow-md"></i><p class="text-slate-500 font-bold text-lg tracking-wide uppercase">Fetching Asset Data...</p></div>`;
document.getElementById('content-hardware').innerHTML = document.getElementById('content-network').innerHTML = document.getElementById('content-software').innerHTML = document.getElementById('content-hotfixes').innerHTML = document.getElementById('content-remote-desktop').innerHTML = loader;
document.getElementById('agentModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
fetch(`?action=get_specs&agent_id=${agentId}`).then(res => res.json()).then(data => {
if (data.error) throw new Error(data.error);
// --- 1. Hardware & Disk Tab ---
let hwHtml = `<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">`;
if (data.hardware) {
const hw = data.hardware;
const ramUsage = hw.ram?.usage || 0;
const ramColor = ramUsage > 85 ? 'text-danger' : (ramUsage > 60 ? 'text-warning' : 'text-success');
const ramBgColor = ramUsage > 85 ? 'bg-danger' : (ramUsage > 60 ? 'bg-warning' : 'bg-success');
hwHtml += `
<div class="col-span-1 lg:col-span-2">
<div class="bg-white border border-slate-200 border-l-4 border-l-primary rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
<h6 class="text-primary font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-microchip mr-2"></i> Processor & Motherboard</h6>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Processor</p><p class="text-slate-900 font-bold m-0 text-lg">${hw.cpu?.name || 'Unknown'}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Cores / Speed</p><p class="text-slate-900 font-bold m-0 text-lg">${hw.cpu?.cores || '-'} Cores / ${hw.cpu?.mhz ? hw.cpu.mhz+' MHz' : '-'}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Board Serial</p><p class="text-slate-900 font-bold m-0"><code class="bg-slate-100 text-slate-700 px-3 py-1 rounded-lg border border-slate-200 shadow-inner">${hw.board_serial || '-'}</code></p></div>
</div>
</div>
</div>
<div class="col-span-1 lg:col-span-2">
<div class="bg-white border border-slate-200 border-l-4 border-l-success rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
<h6 class="text-success font-black text-base mb-2 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-memory mr-2"></i> Memory (RAM)</h6>
<div class="flex justify-between items-end mb-2 mt-5">
<span class="text-xs text-slate-500 uppercase tracking-wider font-bold">Usage</span>
<span class="text-2xl font-black ${ramColor}">${ramUsage}%</span>
</div>
<div class="w-full bg-slate-100 rounded-full h-3 mb-6 shadow-inner overflow-hidden border border-slate-200">
<div class="${ramBgColor} h-3 rounded-full transition-all duration-1000" style="width: ${ramUsage}%"></div>
</div>
<div class="grid grid-cols-2 gap-6 border-t border-slate-100 pt-5">
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Total</p><p class="text-slate-900 font-black m-0 text-xl">${formatKB(hw.ram?.total)}</p></div>
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Free</p><p class="text-slate-900 font-black m-0 text-xl">${formatKB(hw.ram?.free)}</p></div>
</div>
</div>
</div>`;
}
hwHtml += `
<div class="col-span-1 lg:col-span-2">
<div class="bg-white border border-slate-200 border-l-4 border-l-info rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
<h6 class="text-info font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-hard-drive mr-2"></i> Storage Drives (Drive C Only)</h6>`;
let diskData = data.disk_info;
if (diskData && typeof diskData === 'string') {
try {
diskData = JSON.parse(diskData);
} catch (e) {}
}
if (diskData && !Array.isArray(diskData) && typeof diskData === 'object') {
diskData = [diskData];
}
if (diskData && Array.isArray(diskData)) {
diskData = diskData.filter(disk => {
let d = String(disk.DriveLetter || disk.Drive || disk.DeviceID || disk.Name || '');
return d.toUpperCase().includes('C');
});
}
if (diskData && Array.isArray(diskData) && diskData.length > 0) {
hwHtml += `<div class="grid grid-cols-1 md:grid-cols-2 gap-5">`;
diskData.forEach(disk => {
let driveLetter = disk.DriveLetter || disk.Drive || disk.DeviceID || disk.Name || 'C:';
let model = disk.Model || disk.Description || disk.FileSystem || 'Local Disk';
let health = disk.HealthStatus || disk.Status || 'Healthy';
let total = parseFloat(disk.Total_GB || disk.SizeGB || (disk.Size ? disk.Size / 1073741824 : 0));
let free = parseFloat(disk.Free_GB || disk.FreeGB || (disk.FreeSpace ? disk.FreeSpace / 1073741824 : 0));
let used = parseFloat(disk.Used_GB || (total - free));
let usagePercent = total > 0 ? ((used / total) * 100).toFixed(1) : 0;
let barColor = usagePercent > 85 ? 'bg-danger' : (usagePercent > 70 ? 'bg-warning' : 'bg-success');
let healthColor = health.toLowerCase() === 'healthy' || health.toLowerCase() === 'ok' ? 'text-success' : 'text-danger';
total = total > 0 ? total.toFixed(2) : 0;
free = free > 0 ? free.toFixed(2) : 0;
hwHtml += `
<div class="bg-gradient-to-b from-white to-slate-50 border border-slate-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-4">
<div>
<strong class="text-info font-black text-xl flex items-center"><i class="fa-solid fa-server mr-2 text-slate-400"></i> Drive ${driveLetter}</strong>
<div class="text-sm text-slate-500 mt-1.5 font-medium"><i class="fa-solid fa-microchip mr-1"></i> ${model}</div>
</div>
<div class="text-right flex flex-col items-end gap-2">
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold text-white ${barColor} shadow-sm">${usagePercent}% Used</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-bold bg-white border border-slate-200 text-slate-700 shadow-sm"><i class="fa-solid fa-heart-pulse mr-1.5 ${healthColor}"></i> ${health}</span>
</div>
</div>
<div class="w-full bg-slate-200 border border-slate-300 rounded-full h-2 mb-4 overflow-hidden shadow-inner"><div class="${barColor} h-2 rounded-full transition-all duration-1000" style="width: ${usagePercent}%;"></div></div>
<div class="flex justify-between text-sm font-bold">
<span class="text-success"><i class="fa-solid fa-circle-check mr-1"></i> Free: ${free} GB</span>
<span class="text-slate-600">Total: ${total} GB</span>
</div>
</div>`;
});
hwHtml += `</div>`;
} else {
hwHtml += `<div class="bg-slate-50 border border-slate-200 text-slate-500 p-5 rounded-xl flex items-center text-sm font-bold shadow-inner"><i class="fa-solid fa-info-circle text-info mr-3 text-2xl"></i> ไม่พบข้อมูล Drive C (รอรับ Log จาก Agent)</div>`;
}
hwHtml += `</div></div></div>`;
document.getElementById('content-hardware').innerHTML = hwHtml;
// --- 2. Network Tab ---
let netMap = {};
(data.netiface || []).forEach(net => {
if (net.state === 'up') netMap[net.name] = {
name: net.name,
mac: net.mac || '-',
mtu: net.mtu || '-',
rx_bytes: net.rx_bytes || 0,
tx_bytes: net.tx_bytes || 0,
ipv4: [],
ipv6: [],
gateway: '-',
dhcp: '-'
};
});
(data.netaddr || []).forEach(addr => {
let ifaceName = addr.iface || addr.name;
if (netMap[ifaceName]) {
if (addr.proto === 'ipv4' || !addr.address.includes(':')) netMap[ifaceName].ipv4.push({
ip: addr.address,
mask: addr.netmask
});
else netMap[ifaceName].ipv6.push(addr.address);
}
});
let activeNets = Object.values(netMap).filter(net => {
const name = (net.name || '').toLowerCase();
const vp = ['lo', 'veth', 'docker', 'br-', 'vmnet', 'vbox', 'tun', 'tap', 'wg', 'tailscale'];
return !(vp.some(p => name.startsWith(p)) || name.includes('loopback') || name.includes('virtual') || name.includes('teredo') || name.includes('vmware'));
});
if (activeNets.length > 0) {
let netHtml = `<div class="grid grid-cols-1 md:grid-cols-2 gap-5">`;
activeNets.forEach(net => {
let ipv4List = net.ipv4.length > 0 ? net.ipv4.map(ip => `<code class="bg-blue-50 px-2 py-0.5 rounded border border-blue-100 text-primary font-bold shadow-sm">${ip.ip}</code>`).join(' ') : 'No IPv4';
netHtml += `
<div class="bg-white border border-slate-200 border-l-4 border-l-success rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
<div class="flex justify-between items-center mb-5 pb-3 border-b border-slate-100">
<h6 class="text-success font-black text-lg m-0 uppercase tracking-wide"><i class="fa-solid fa-ethernet mr-2"></i>${net.name}</h6>
<span class="bg-gradient-to-r from-emerald-500 to-green-600 text-white inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold shadow-sm">UP</span>
</div>
<div class="space-y-4 text-sm">
<div class="flex justify-between items-center"><span class="text-slate-500 font-bold uppercase tracking-wider text-xs">MAC Address:</span> <span class="font-mono font-bold text-slate-700 bg-slate-50 border border-slate-200 px-2 py-1 rounded shadow-inner">${net.mac}</span></div>
<div class="flex justify-between items-start pt-3 border-t border-slate-50"><span class="text-slate-500 font-bold uppercase tracking-wider text-xs mt-1">IPv4 Address:</span> <div class="flex flex-col items-end gap-2">${ipv4List}</div></div>
</div>
</div>`;
});
document.getElementById('content-network').innerHTML = netHtml + `</div>`;
} else {
document.getElementById('content-network').innerHTML = `<div class="bg-blue-50 border border-blue-200 text-primary p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4"></i> No active physical networks found.</div>`;
}
// --- 3. Software Tab ---
if ($.fn.DataTable.isDataTable('#dt-software-modal')) {
$('#dt-software-modal').DataTable().destroy();
}
if (data.packages && data.packages.length > 0) {
let softHtml = `<div class="overflow-x-auto bg-white border border-slate-200 rounded-xl p-5 shadow-sm"><table id="dt-software-modal" class="w-full text-left"><thead><tr><th class="text-left pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Software Name</th><th class="pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Version</th><th class="pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Vendor</th></tr></thead><tbody class="text-sm">`;
data.packages.forEach(pkg => {
softHtml += `<tr class="border-b border-slate-100 hover:bg-slate-50 transition-colors"><td class="py-4 font-bold text-slate-800">${pkg.name || '-'}</td><td class="py-4 text-slate-600 font-medium">${pkg.version || '-'}</td><td class="py-4 text-slate-500 font-medium">${pkg.vendor || '-'}</td></tr>`;
});
document.getElementById('content-software').innerHTML = softHtml + `</tbody></table></div>`;
$('#dt-software-modal').DataTable({
pageLength: 10,
language: {
search: "_INPUT_",
searchPlaceholder: "Search software..."
}
});
} else {
document.getElementById('content-software').innerHTML = `<div class="bg-blue-50 border border-blue-200 text-primary p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4"></i> No Software data available.</div>`;
}
// --- 4. Hotfixes Tab ---
if ($.fn.DataTable.isDataTable('#dt-hotfixes-modal')) {
$('#dt-hotfixes-modal').DataTable().destroy();
}
if (data.hotfixes && data.hotfixes.length > 0) {
let hfHtml = `<div class="overflow-x-auto bg-white border border-slate-200 rounded-xl p-5 shadow-sm"><table id="dt-hotfixes-modal" class="w-full text-left"><thead><tr><th class="text-left pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Hotfix ID</th><th class="pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Description</th><th class="text-center pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Action</th></tr></thead><tbody class="text-sm">`;
data.hotfixes.forEach(hf => {
let kbID = hf.hotfix || '-';
let desc = hf.description || hf.caption || 'System Update';
let kbLink = '-';
if (kbID.toUpperCase().includes('KB')) {
kbLink = `<a href="https://support.microsoft.com/help/${kbID.replace(/\D/g, '')}" target="_blank" class="inline-flex items-center px-4 py-1.5 text-xs font-bold rounded-lg text-primary bg-blue-50 hover:bg-primary hover:text-white transition-colors border border-blue-200 shadow-sm"><i class="fa-solid fa-arrow-up-right-from-square mr-1.5"></i> Info</a>`;
} else if (kbID !== '-') {
kbLink = `<a href="https://www.google.com/search?q=Windows+Update+${kbID}" target="_blank" class="inline-flex items-center px-4 py-1.5 text-xs font-bold rounded-lg text-slate-600 bg-slate-100 border border-slate-300 hover:bg-slate-200 transition-colors shadow-sm"><i class="fa-brands fa-google mr-1.5"></i> Search</a>`;
}
hfHtml += `<tr class="border-b border-slate-100 hover:bg-slate-50 transition-colors"><td class="py-4 font-black text-slate-800"><i class="fa-solid fa-shield-virus mr-2 text-success text-lg"></i>${kbID}</td><td class="py-4 text-slate-600 font-medium">${desc}</td><td class="py-4 text-center">${kbLink}</td></tr>`;
});
document.getElementById('content-hotfixes').innerHTML = hfHtml + `</tbody></table></div>`;
$('#dt-hotfixes-modal').DataTable({
pageLength: 10,
language: {
search: "_INPUT_",
searchPlaceholder: "Search updates..."
}
});
} else {
document.getElementById('content-hotfixes').innerHTML = `<div class="bg-blue-50 border border-blue-200 text-primary p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4"></i> No Windows Update data available.</div>`;
}
// --- 🌟 5. Remote Desktop Tab (TeamViewer & AnyDesk) 🌟 ---
let tvId = (meta && meta.tv && meta.tv !== 'None') ? meta.tv : 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง';
let tvActive = (tvId !== 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง');
let adId = (meta && meta.ad && meta.ad !== 'None') ? meta.ad : 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง';
let adActive = (adId !== 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง');
let rdHtml = `
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white border border-slate-200 border-l-4 rounded-xl p-8 shadow-sm transition-all hover:shadow-md ${tvActive ? 'border-l-primary' : 'border-l-slate-300 opacity-75 grayscale-[50%]'}">
<div class="flex items-center mb-6 pb-4 border-b border-slate-100">
<div class="w-12 h-12 rounded-xl flex items-center justify-center mr-4 shadow-inner ${tvActive ? 'bg-gradient-to-br from-blue-500 to-blue-700 text-white' : 'bg-slate-100 text-slate-400 border border-slate-200'}"><i class="fa-solid fa-people-arrows text-2xl"></i></div>
<h6 class="font-black text-xl m-0 tracking-wide ${tvActive ? 'text-slate-900' : 'text-slate-500'}">TeamViewer</h6>
</div>
<div class="bg-slate-50 p-5 rounded-xl border border-slate-100 shadow-inner">
<p class="text-xs text-slate-500 uppercase tracking-wider mb-2 font-bold">TeamViewer ID (Client ID)</p>
<p class="font-black text-3xl m-0 ${tvActive ? 'text-primary' : 'text-slate-400'} tracking-widest font-mono drop-shadow-sm">${tvId}</p>
</div>
</div>
<div class="bg-white border border-slate-200 border-l-4 rounded-xl p-8 shadow-sm transition-all hover:shadow-md ${adActive ? 'border-l-danger' : 'border-l-slate-300 opacity-75 grayscale-[50%]'}">
<div class="flex items-center mb-6 pb-4 border-b border-slate-100">
<div class="w-12 h-12 rounded-xl flex items-center justify-center mr-4 shadow-inner ${adActive ? 'bg-gradient-to-br from-red-500 to-red-700 text-white' : 'bg-slate-100 text-slate-400 border border-slate-200'}"><i class="fa-solid fa-desktop text-2xl"></i></div>
<h6 class="font-black text-xl m-0 tracking-wide ${adActive ? 'text-slate-900' : 'text-slate-500'}">AnyDesk</h6>
</div>
<div class="bg-slate-50 p-5 rounded-xl border border-slate-100 shadow-inner">
<p class="text-xs text-slate-500 uppercase tracking-wider mb-2 font-bold">AnyDesk Address (ID)</p>
<p class="font-black text-3xl m-0 ${adActive ? 'text-danger' : 'text-slate-400'} tracking-widest font-mono drop-shadow-sm">${adId}</p>
</div>
</div>
</div>`;
document.getElementById('content-remote-desktop').innerHTML = rdHtml;
}).catch(err => {
const errorMsg = `<div class="bg-red-50 border border-red-200 text-danger p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-xmark text-3xl mr-4"></i> Error Fetching Data</div>`;
document.getElementById('content-hardware').innerHTML = document.getElementById('content-network').innerHTML = document.getElementById('content-software').innerHTML = document.getElementById('content-hotfixes').innerHTML = document.getElementById('content-remote-desktop').innerHTML = errorMsg;
});
}
</script>
</body>
</html>