mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Auto-populate network devices via nmap scan from PVE1 every 3min
This commit is contained in:
@@ -311,6 +311,62 @@ function collect_all(): array {
|
||||
$results['sites'] = 'error: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
|
||||
// ── Network Device Scan (nmap via PVE1) ───────────────────────────────
|
||||
try {
|
||||
$nmapRaw = shell_exec(
|
||||
"sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 " .
|
||||
"root@10.48.200.90 'nmap -sn --send-ip 10.48.200.0/24 2>/dev/null' 2>/dev/null"
|
||||
);
|
||||
if ($nmapRaw) {
|
||||
$discovered = [];
|
||||
$cur = null;
|
||||
foreach (explode("\n", $nmapRaw) as $line) {
|
||||
$line = trim($line);
|
||||
if (preg_match('/Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?/', $line, $m)) {
|
||||
if ($cur) $discovered[] = $cur;
|
||||
$cur = ['hostname' => ($m[1] && $m[1] !== $m[2]) ? $m[1] : null, 'ip' => $m[2], 'mac' => null, 'vendor' => null];
|
||||
} elseif ($cur && preg_match('/MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)/i', $line, $m)) {
|
||||
$cur['mac'] = strtolower($m[1]);
|
||||
$cur['vendor'] = $m[2] !== 'Unknown' ? $m[2] : null;
|
||||
}
|
||||
}
|
||||
if ($cur) $discovered[] = $cur;
|
||||
|
||||
$discoveredIPs = [];
|
||||
foreach ($discovered as $d) {
|
||||
$discoveredIPs[] = $d['ip'];
|
||||
JarvisDB::execute(
|
||||
'INSERT INTO network_devices (ip, mac, hostname, status, last_seen)
|
||||
VALUES (?,?,?,"online",NOW())
|
||||
ON DUPLICATE KEY UPDATE mac=VALUES(mac), hostname=COALESCE(VALUES(hostname),hostname),
|
||||
status="online", last_seen=NOW()',
|
||||
[$d['ip'], $d['mac'], $d['hostname'] ?? $d['vendor']]
|
||||
);
|
||||
if ($d['vendor']) {
|
||||
JarvisDB::execute(
|
||||
'UPDATE network_devices SET device_type=? WHERE ip=? AND (device_type IS NULL OR device_type="")',
|
||||
[$d['vendor'], $d['ip']]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($discoveredIPs)) {
|
||||
$ph = implode(',', array_fill(0, count($discoveredIPs), '?'));
|
||||
JarvisDB::execute(
|
||||
"UPDATE network_devices SET status='offline'
|
||||
WHERE ip NOT IN ($ph) AND last_seen < DATE_SUB(NOW(), INTERVAL 10 MINUTE)",
|
||||
$discoveredIPs
|
||||
);
|
||||
}
|
||||
$results['nmap_scan'] = 'ok (' . count($discovered) . ' devices found)';
|
||||
} else {
|
||||
$results['nmap_scan'] = 'skipped (PVE1 unreachable)';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$results['nmap_scan'] = 'error: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
+107
-30
@@ -10,6 +10,17 @@ function loggedIn(): bool { return !empty($_SESSION['admin_user']); }
|
||||
function j(mixed $d): never { header('Content-Type: application/json'); echo json_encode($d); exit; }
|
||||
function bad(string $msg, int $code = 400): never { http_response_code($code); j(['error' => $msg]); }
|
||||
|
||||
function self_upsert_device(array $d): void {
|
||||
JarvisDB::execute(
|
||||
'INSERT INTO network_devices (ip,mac,hostname,status,last_seen) VALUES (?,?,?,"online",NOW())
|
||||
ON DUPLICATE KEY UPDATE mac=VALUES(mac), hostname=COALESCE(VALUES(hostname),hostname), status="online", last_seen=NOW()',
|
||||
[$d['ip'], $d['mac'], $d['hostname'] ?? $d['vendor']]
|
||||
);
|
||||
if (!empty($d['vendor'])) {
|
||||
JarvisDB::execute('UPDATE network_devices SET device_type=? WHERE ip=? AND (device_type IS NULL OR device_type="")', [$d['vendor'], $d['ip']]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── BACKEND API ───────────────────────────────────────────────────────────────
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||
if ($action) {
|
||||
@@ -74,7 +85,26 @@ if ($action) {
|
||||
|
||||
// ── NETWORK ──────────────────────────────────────────────────────────
|
||||
case 'network_list':
|
||||
j(JarvisDB::query('SELECT id,ip,mac,hostname,alias,device_type,status,last_seen FROM network_devices WHERE alias IS NOT NULL ORDER BY alias'));
|
||||
j(JarvisDB::query('SELECT id,ip,mac,hostname,alias,device_type,status,last_seen FROM network_devices ORDER BY status="online" DESC, COALESCE(alias,hostname,ip)'));
|
||||
|
||||
case 'network_scan':
|
||||
// Trigger immediate nmap scan via PVE1
|
||||
$out = shell_exec("sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@10.48.200.90 'nmap -sn --send-ip 10.48.200.0/24 2>/dev/null' 2>/dev/null");
|
||||
$count = 0;
|
||||
if ($out) {
|
||||
$cur = null;
|
||||
foreach (explode("\n", $out) as $line) {
|
||||
$line = trim($line);
|
||||
if (preg_match('/Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?/', $line, $m)) {
|
||||
if ($cur) { self_upsert_device($cur); $count++; }
|
||||
$cur = ['hostname' => ($m[1] && $m[1] !== $m[2]) ? $m[1] : null, 'ip' => $m[2], 'mac' => null, 'vendor' => null];
|
||||
} elseif ($cur && preg_match('/MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)/i', $line, $m)) {
|
||||
$cur['mac'] = strtolower($m[1]); $cur['vendor'] = $m[2] !== 'Unknown' ? $m[2] : null;
|
||||
}
|
||||
}
|
||||
if ($cur) { self_upsert_device($cur); $count++; }
|
||||
}
|
||||
j(['ok' => true, 'found' => $count]);
|
||||
|
||||
case 'network_save':
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
@@ -406,9 +436,18 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
||||
<div class="page-title">NETWORK DEVICES
|
||||
<div class="actions">
|
||||
<button class="btn btn-sm btn-green" onclick="netModal()">+ ADD DEVICE</button>
|
||||
<button class="btn btn-sm btn-yellow" id="scanBtn" onclick="scanNow()">SCAN NOW</button>
|
||||
<button class="btn btn-sm" onclick="loadNetwork()">REFRESH</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters" style="margin-bottom:12px">
|
||||
<span class="lbl">FILTER:</span>
|
||||
<button class="filter-btn active" id="nf-all" onclick="setNetFilter('all',this)">ALL</button>
|
||||
<button class="filter-btn" id="nf-online" onclick="setNetFilter('online',this)">ONLINE</button>
|
||||
<button class="filter-btn" id="nf-offline" onclick="setNetFilter('offline',this)">OFFLINE</button>
|
||||
<button class="filter-btn" id="nf-named" onclick="setNetFilter('named',this)">NAMED</button>
|
||||
<span class="lbl" id="net-count" style="color:var(--cyan)"></span>
|
||||
</div>
|
||||
<div class="tbl-wrap" id="network-tbl"><div class="loading">LOADING...</div></div>
|
||||
</div>
|
||||
|
||||
@@ -566,10 +605,12 @@ function nav(el) {
|
||||
}
|
||||
|
||||
function loadTab(tab) {
|
||||
// Stop any existing network auto-refresh when leaving
|
||||
if (_netAutoRefresh) { clearInterval(_netAutoRefresh); _netAutoRefresh = null; }
|
||||
({
|
||||
dashboard: loadDashboard,
|
||||
agents: loadAgents,
|
||||
network: loadNetwork,
|
||||
network: ()=>{ loadNetwork(); _netAutoRefresh = setInterval(loadNetwork, 30000); },
|
||||
alerts: loadAlerts,
|
||||
facts: ()=>{ loadFactCategories(); loadFacts(); },
|
||||
intents: loadIntents,
|
||||
@@ -666,29 +707,74 @@ function delAgent(id, name) {
|
||||
}
|
||||
|
||||
// ── NETWORK ───────────────────────────────────────────────────────────────────
|
||||
let _netFilter = 'all';
|
||||
let _allDevices = [];
|
||||
let _netAutoRefresh = null;
|
||||
|
||||
function setNetFilter(f, el) {
|
||||
_netFilter = f;
|
||||
document.querySelectorAll('#tab-network .filter-btn').forEach(b=>b.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
renderNetwork();
|
||||
}
|
||||
|
||||
async function loadNetwork() {
|
||||
document.getElementById('network-tbl').innerHTML = '<div class="loading">LOADING...</div>';
|
||||
const devs = await api('network_list');
|
||||
if (!devs.length) { document.getElementById('network-tbl').innerHTML='<div class="empty">NO NAMED DEVICES</div>'; return; }
|
||||
let rows = devs.map(d => `<tr>
|
||||
<td><span class="dot ${d.status==='online'?'dot-green':d.status==='offline'?'dot-red':'dot-dim'}"></span>${esc(d.alias)}</td>
|
||||
<td>${esc(d.ip)}</td>
|
||||
<td><span class="badge badge-dim">${esc(d.device_type||'device').toUpperCase()}</span></td>
|
||||
<td>${statusBadge(d.status)}</td>
|
||||
<td class="ts">${ago(d.last_seen)}</td>
|
||||
<td><div class="actions-col">
|
||||
<button class="btn btn-xs" onclick="pingDev(${d.id},'${esc(d.ip)}',this)">PING</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${esc(d.ip)}','${esc(d.alias)}','${esc(d.device_type||'')}')">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="delNet(${d.id},'${esc(d.alias)}')">DEL</button>
|
||||
</div></td>
|
||||
</tr>`).join('');
|
||||
_allDevices = await api('network_list');
|
||||
renderNetwork();
|
||||
}
|
||||
|
||||
function renderNetwork() {
|
||||
let devs = _allDevices;
|
||||
if (_netFilter === 'online') devs = devs.filter(d => d.status === 'online');
|
||||
if (_netFilter === 'offline') devs = devs.filter(d => d.status === 'offline');
|
||||
if (_netFilter === 'named') devs = devs.filter(d => d.alias);
|
||||
|
||||
const onlineCount = _allDevices.filter(d=>d.status==='online').length;
|
||||
document.getElementById('net-count').textContent = `${onlineCount}/${_allDevices.length} ONLINE`;
|
||||
|
||||
if (!devs.length) { document.getElementById('network-tbl').innerHTML='<div class="empty">NO DEVICES MATCH FILTER</div>'; return; }
|
||||
let rows = devs.map(d => {
|
||||
const name = d.alias || d.hostname || d.ip;
|
||||
const isNamed = !!d.alias;
|
||||
const vendor = d.device_type || '—';
|
||||
return `<tr>
|
||||
<td>
|
||||
<span class="dot ${d.status==='online'?'dot-green':d.status==='offline'?'dot-red':'dot-dim'}"></span>
|
||||
<strong>${esc(name)}</strong>${isNamed?'':' <span style="color:var(--dim);font-size:0.6rem">(discovered)</span>'}
|
||||
</td>
|
||||
<td style="color:var(--cyan)">${esc(d.ip)}</td>
|
||||
<td style="font-size:0.65rem;color:var(--dim)">${esc(d.mac||'—')}</td>
|
||||
<td class="trunc ts" style="max-width:140px" title="${esc(vendor)}">${esc(vendor)}</td>
|
||||
<td>${statusBadge(d.status)}</td>
|
||||
<td class="ts">${ago(d.last_seen)}</td>
|
||||
<td><div class="actions-col">
|
||||
<button class="btn btn-xs" onclick="pingDev('${esc(d.ip)}',this)">PING</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${esc(d.ip)}','${esc(d.alias||'')}','${esc(d.device_type||'')}')">NAME</button>
|
||||
<button class="btn btn-xs btn-red" onclick="delNet(${d.id},'${esc(name)}')">DEL</button>
|
||||
</div></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
document.getElementById('network-tbl').innerHTML = `<table>
|
||||
<thead><tr><th>NAME</th><th>IP</th><th>TYPE</th><th>STATUS</th><th>LAST SEEN</th><th>ACTIONS</th></tr></thead>
|
||||
<thead><tr><th>NAME</th><th>IP</th><th>MAC</th><th>VENDOR / TYPE</th><th>STATUS</th><th>LAST SEEN</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>${rows}</tbody></table>`;
|
||||
}
|
||||
|
||||
function netModal(id=0, ip='', alias='', type='device') {
|
||||
openModal(id?'EDIT DEVICE':'ADD DEVICE', `
|
||||
async function scanNow() {
|
||||
const btn = document.getElementById('scanBtn');
|
||||
btn.textContent = 'SCANNING...'; btn.disabled = true;
|
||||
const fd = new FormData(); fd.append('action','network_scan');
|
||||
try {
|
||||
const r = await fetch(location.href,{method:'POST',body:fd});
|
||||
const d = await r.json();
|
||||
if (d.ok) { toast(`Scan complete — ${d.found} devices found`,'ok'); loadNetwork(); }
|
||||
else toast(d.error||'Scan failed','err');
|
||||
} catch(e){ toast('Scan failed','err'); }
|
||||
btn.textContent = 'SCAN NOW'; btn.disabled = false;
|
||||
}
|
||||
|
||||
function netModal(id=0, ip='', alias='', type='') {
|
||||
openModal(id?'NAME / EDIT DEVICE':'ADD DEVICE', `
|
||||
<div class="form-row"><label>IP ADDRESS</label><input id="f-ip" value="${esc(ip)}" placeholder="10.48.200.x"></div>
|
||||
<div class="form-row"><label>NAME / ALIAS</label><input id="f-alias" value="${esc(alias)}" placeholder="My Device"></div>
|
||||
<div class="form-row"><label>TYPE</label>
|
||||
@@ -703,16 +789,7 @@ function netModal(id=0, ip='', alias='', type='device') {
|
||||
});
|
||||
}
|
||||
|
||||
async function pingDev(id, ip, btn) {
|
||||
btn.textContent = '...'; btn.disabled = true;
|
||||
const res = await apiPost('network_ping', {ip}, null);
|
||||
// Re-fetch to update status
|
||||
loadNetwork();
|
||||
btn.textContent = 'PING'; btn.disabled = false;
|
||||
}
|
||||
|
||||
// Direct ping returning result
|
||||
(window.pingDev = async function(id, ip, btn) {
|
||||
async function pingDev(ip, btn) {
|
||||
btn.textContent='…'; btn.disabled=true;
|
||||
const fd=new FormData(); fd.append('action','network_ping'); fd.append('ip',ip);
|
||||
try {
|
||||
@@ -721,7 +798,7 @@ async function pingDev(id, ip, btn) {
|
||||
toast(d.alive ? `${ip} ONLINE (${d.latency_ms??'?'}ms)` : `${ip} OFFLINE`, d.alive?'ok':'err');
|
||||
} catch(e){ toast('Ping failed','err'); }
|
||||
btn.textContent='PING'; btn.disabled=false;
|
||||
});
|
||||
}
|
||||
|
||||
function delNet(id, name) {
|
||||
if (!confirm(`Remove "${name}" from network devices?`)) return;
|
||||
|
||||
Reference in New Issue
Block a user