1332 lines
39 KiB
JavaScript
1332 lines
39 KiB
JavaScript
const { invoke } = window.__TAURI__.core;
|
|
const { listen } = window.__TAURI__.event;
|
|
var theme = (() => {
|
|
var STORAGE_KEY = 'argus-theme';
|
|
|
|
function get() {
|
|
try {
|
|
return localStorage.getItem(STORAGE_KEY) || 'cyber';
|
|
} catch (e) {
|
|
return 'cyber';
|
|
}
|
|
}
|
|
|
|
function set(name) {
|
|
if (name === 'cyber') {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme', name);
|
|
}
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, name);
|
|
} catch (e) { }
|
|
}
|
|
|
|
var HACKER_KEY = 'argus-hacker-unlocked';
|
|
|
|
function isHackerUnlocked() {
|
|
try {
|
|
return localStorage.getItem(HACKER_KEY) === 'true';
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function unlockHacker() {
|
|
try {
|
|
localStorage.setItem(HACKER_KEY, 'true');
|
|
} catch (e) { }
|
|
addHackerOption();
|
|
}
|
|
|
|
function addHackerOption() {
|
|
var picker = document.getElementById('theme-picker');
|
|
if (!picker.querySelector('option[value="hacker"]')) {
|
|
var opt = document.createElement('option');
|
|
opt.value = 'hacker';
|
|
opt.textContent = '\u2588 H4CK3R';
|
|
picker.appendChild(opt);
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
var saved = get();
|
|
set(saved);
|
|
|
|
// Add hacker option if previously unlocked
|
|
if (isHackerUnlocked()) {
|
|
addHackerOption();
|
|
}
|
|
|
|
var picker = document.getElementById('theme-picker');
|
|
picker.value = saved;
|
|
picker.addEventListener('change', function () {
|
|
set(picker.value);
|
|
});
|
|
|
|
// Easter egg: listen on the scan input
|
|
var scanInput = document.getElementById('ip-range-input');
|
|
scanInput.addEventListener('input', function () {
|
|
var val = scanInput.value.toLowerCase().trim();
|
|
if (val === 'all your base are belong to us') {
|
|
if (!isHackerUnlocked()) {
|
|
unlockHacker();
|
|
set('hacker');
|
|
picker.value = 'hacker';
|
|
scanInput.value = '';
|
|
console.log('%c> ACCESS GRANTED', 'color: #00ff41; font-size: 20px; font-family: monospace;');
|
|
console.log('%c> HACKER THEME UNLOCKED', 'color: #00ff41; font-size: 14px; font-family: monospace;');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Apply immediately (before DOMContentLoaded) to prevent flash
|
|
set(get());
|
|
|
|
return { init };
|
|
})();
|
|
const table = (() => {
|
|
var dirty = false;
|
|
var flushTimer = null;
|
|
var ipIndex = {};
|
|
let data = [];
|
|
let filtered = [];
|
|
let sortKey = 'ip';
|
|
let sortDir = 'asc';
|
|
let filterText = '';
|
|
let checkedIps = new Set();
|
|
var ROW_HEIGHT = 32;
|
|
var BUFFER_ROWS = 20;
|
|
var scrollContainer = null;
|
|
|
|
function ipToNum(ip) {
|
|
const parts = String(ip).split('.');
|
|
if (parts.length !== 4) return 0;
|
|
return parts.reduce((acc, o) => (acc << 8) + parseInt(o, 10), 0) >>> 0;
|
|
}
|
|
|
|
function getSortVal(host, key) {
|
|
switch (key) {
|
|
case 'ip': return ipToNum(host.ip);
|
|
case 'status': return host.status || '';
|
|
case 'rtt': return host.ping ? host.ping.rtt_ms : 99999;
|
|
case 'hostname': return (host.hostname || '').toLowerCase();
|
|
case 'ports': return host.ports ? host.ports.filter((p) => p.state === 'Open').length : 0;
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
function formatPorts(host) {
|
|
if (!host.ports || host.ports.length === 0) return '-';
|
|
const open = host.ports.filter((p) => p.state === 'Open');
|
|
if (open.length === 0) return '-';
|
|
return open.map((p) => {
|
|
const svc = p.service ? p.service : p.port;
|
|
return '<span class="port-tag">' + p.port + '/' + (p.service || 'unknown') + '</span>';
|
|
}).join(' ');
|
|
}
|
|
|
|
function buildDetailHtml(host) {
|
|
let html = '<div class="detail-panel">';
|
|
html += '<div class="detail-header">';
|
|
html += '<span class="detail-ip">' + host.ip + '</span>';
|
|
if (host.hostname) html += '<span class="detail-hostname">' + host.hostname + '</span>';
|
|
if (host.ping) html += '<span class="detail-rtt">' + host.ping.rtt_ms.toFixed(1) + ' ms</span>';
|
|
html += '</div>';
|
|
if (host.netbios) {
|
|
html += '<div class="detail-netbios">';
|
|
if (host.netbios.computer_name)
|
|
html += '<span class="detail-tag">Computer: <strong>' + host.netbios.computer_name + '</strong></span>';
|
|
if (host.netbios.workgroup)
|
|
html += '<span class="detail-tag">Workgroup: <strong>' + host.netbios.workgroup + '</strong></span>';
|
|
if (host.netbios.mac_from_netbios)
|
|
html += '<span class="detail-tag">MAC: <strong>' + host.netbios.mac_from_netbios + '</strong></span>';
|
|
html += '</div>';
|
|
}
|
|
if (host.ports && host.ports.length > 0) {
|
|
const open = host.ports.filter((p) => p.state === 'Open');
|
|
const closed = host.ports.filter((p) => p.state === 'Closed');
|
|
const filtered = host.ports.filter((p) => p.state === 'Filtered');
|
|
|
|
if (open.length > 0) {
|
|
html += '<table class="detail-table">';
|
|
html += '<thead><tr>';
|
|
html += '<th>Port</th><th>Protocol</th><th>State</th><th>Service</th><th>Banner</th>';
|
|
html += '</tr></thead><tbody>';
|
|
for (const p of open) {
|
|
html += '<tr>';
|
|
html += '<td>' + p.port + '</td>';
|
|
html += '<td>' + p.protocol + '</td>';
|
|
html += '<td class="status-up">' + p.state + '</td>';
|
|
html += '<td>' + (p.service || '-') + '</td>';
|
|
html += '<td>' + (p.banner || '-') + '</td>';
|
|
html += '</tr>';
|
|
}
|
|
html += '</tbody></table>';
|
|
}
|
|
|
|
html += '<div class="detail-summary">';
|
|
html += '<span>' + open.length + ' open</span>';
|
|
html += '<span>' + closed.length + ' closed</span>';
|
|
html += '<span>' + filtered.length + ' filtered</span>';
|
|
html += '</div>';
|
|
} else {
|
|
html += '<p class="detail-empty">No port scan data. Enable "Simple Port Scan" and rescan.</p>';
|
|
}
|
|
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
let expandedIp = null;
|
|
|
|
function handleRowClick(e) {
|
|
const row = e.target.closest('tr');
|
|
if (!row || !row.dataset.ip) return;
|
|
const ip = row.dataset.ip;
|
|
|
|
// Remove any existing detail row
|
|
const existing = document.querySelector('.detail-row');
|
|
if (existing) existing.remove();
|
|
|
|
// If clicking the same row, just collapse
|
|
if (expandedIp === ip) {
|
|
expandedIp = null;
|
|
row.classList.remove('selected');
|
|
return;
|
|
}
|
|
|
|
// Deselect previous
|
|
const prev = document.querySelector('#results-body tr.selected');
|
|
if (prev) prev.classList.remove('selected');
|
|
|
|
// Find the host data
|
|
const host = data.find((h) => String(h.ip) === ip);
|
|
if (!host) return;
|
|
|
|
// Mark selected
|
|
row.classList.add('selected');
|
|
expandedIp = ip;
|
|
|
|
// Insert detail row after the clicked row
|
|
const detailRow = document.createElement('tr');
|
|
detailRow.className = 'detail-row';
|
|
const td = document.createElement('td');
|
|
td.colSpan = document.querySelectorAll('#results-table th').length;
|
|
td.innerHTML = buildDetailHtml(host);
|
|
detailRow.appendChild(td);
|
|
row.after(detailRow);
|
|
}
|
|
|
|
function refresh() {
|
|
if (filterText) {
|
|
const q = filterText.toLowerCase();
|
|
filtered = data.filter((h) =>
|
|
String(h.ip).includes(q) ||
|
|
(h.hostname || '').toLowerCase().includes(q) ||
|
|
(h.status || '').toLowerCase().includes(q)
|
|
);
|
|
} else {
|
|
filtered = [...data];
|
|
}
|
|
|
|
filtered.sort((a, b) => {
|
|
const va = getSortVal(a, sortKey);
|
|
const vb = getSortVal(b, sortKey);
|
|
let cmp = 0;
|
|
if (typeof va === 'number' && typeof vb === 'number') {
|
|
cmp = va - vb;
|
|
} else {
|
|
cmp = String(va).localeCompare(String(vb));
|
|
}
|
|
return sortDir === 'asc' ? cmp : -cmp;
|
|
});
|
|
|
|
render();
|
|
}
|
|
|
|
function render() {
|
|
var tbody = document.getElementById('results-body');
|
|
var count = document.getElementById('host-count');
|
|
if (!tbody) return;
|
|
|
|
expandedIp = null;
|
|
|
|
if (!scrollContainer) {
|
|
scrollContainer = document.getElementById('results-table-container');
|
|
scrollContainer.addEventListener('scroll', onScroll);
|
|
}
|
|
|
|
renderVisible();
|
|
|
|
if (count) {
|
|
var up = data.filter(function (h) { return h.status === 'Up'; }).length;
|
|
count.textContent = up + ' up / ' + data.length + ' total';
|
|
}
|
|
|
|
if (columns && columns.apply) columns.apply();
|
|
}
|
|
|
|
function renderVisible() {
|
|
var tbody = document.getElementById('results-body');
|
|
if (!tbody || !scrollContainer) return;
|
|
|
|
var totalRows = filtered.length;
|
|
var viewHeight = scrollContainer.clientHeight;
|
|
var scrollTop = scrollContainer.scrollTop;
|
|
var headerHeight = 36;
|
|
|
|
var startIdx = Math.max(0, Math.floor((scrollTop - headerHeight) / ROW_HEIGHT) - BUFFER_ROWS);
|
|
var visibleCount = Math.ceil(viewHeight / ROW_HEIGHT) + (BUFFER_ROWS * 2);
|
|
var endIdx = Math.min(totalRows, startIdx + visibleCount);
|
|
|
|
var topSpace = startIdx * ROW_HEIGHT;
|
|
var bottomSpace = (totalRows - endIdx) * ROW_HEIGHT;
|
|
|
|
var colCount = document.querySelectorAll('#results-table th').length;
|
|
|
|
var html = '';
|
|
|
|
if (topSpace > 0) {
|
|
html += '<tr class="virtual-spacer"><td colspan="' + colCount + '" style="height:' + topSpace + 'px"></td></tr>';
|
|
}
|
|
|
|
for (var i = startIdx; i < endIdx; i++) {
|
|
var host = filtered[i];
|
|
var ip = host.ip;
|
|
var status = host.status || 'Unknown';
|
|
var cls = 'status-' + status.toLowerCase();
|
|
var rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-';
|
|
var hostname = host.hostname || '-';
|
|
var isChecked = checkedIps.has(ip) ? 'checked' : '';
|
|
|
|
html += '<tr data-ip="' + ip + '">'
|
|
+ '<td class="col-check"><input type="checkbox" class="row-check" ' + isChecked + ' /></td>'
|
|
+ '<td>' + ip + '</td>'
|
|
+ '<td class="' + cls + '">' + status + '</td>'
|
|
+ '<td>' + rtt + '</td>'
|
|
+ '<td>' + hostname + '</td>'
|
|
+ '<td class="ports-cell">' + formatPorts(host) + '</td>'
|
|
+ '</tr>';
|
|
}
|
|
|
|
if (bottomSpace > 0) {
|
|
html += '<tr class="virtual-spacer"><td colspan="' + colCount + '" style="height:' + bottomSpace + 'px"></td></tr>';
|
|
}
|
|
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
var scrollTimeout = null;
|
|
function onScroll() {
|
|
if (scrollTimeout) return;
|
|
scrollTimeout = requestAnimationFrame(function () {
|
|
scrollTimeout = null;
|
|
renderVisible();
|
|
if (columns && columns.apply) columns.apply();
|
|
});
|
|
}
|
|
|
|
function upsert(host) {
|
|
var idx = data.findIndex(function (h) { return h.ip === host.ip; });
|
|
if (idx >= 0) {
|
|
data[idx] = host;
|
|
} else {
|
|
ipIndex[host.ip] = data.length;
|
|
data.push(host);
|
|
}
|
|
dirty = true;
|
|
if (!flushTimer) {
|
|
flushTimer = setTimeout(flush, 200);
|
|
}
|
|
}
|
|
|
|
function flush() {
|
|
flushTimer = null;
|
|
if (!dirty) return;
|
|
dirty = false;
|
|
refresh();
|
|
}
|
|
|
|
function clear() {
|
|
data = [];
|
|
filtered = [];
|
|
checkedIps.clear();
|
|
ipIndex = {};
|
|
document.getElementById('check-all').checked = false;
|
|
refresh();
|
|
}
|
|
|
|
function setSort(key) {
|
|
if (sortKey === key) {
|
|
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortKey = key;
|
|
sortDir = 'asc';
|
|
}
|
|
document.querySelectorAll('#results-table th').forEach((th) => {
|
|
th.classList.remove('sorted-asc', 'sorted-desc');
|
|
if (th.dataset.sort === sortKey) {
|
|
th.classList.add(sortDir === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
}
|
|
});
|
|
refresh();
|
|
}
|
|
|
|
function setFilter(text) {
|
|
filterText = text;
|
|
refresh();
|
|
}
|
|
|
|
return {
|
|
upsert, clear, setSort, setFilter, handleRowClick,
|
|
init: () => {
|
|
document.getElementById('results-body')
|
|
.addEventListener('click', (e) => {
|
|
// Don't expand detail panel when clicking a checkbox
|
|
if (e.target.classList.contains('row-check')) {
|
|
const row = e.target.closest('tr');
|
|
if (row && row.dataset.ip) {
|
|
if (e.target.checked) {
|
|
checkedIps.add(row.dataset.ip);
|
|
} else {
|
|
checkedIps.delete(row.dataset.ip);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
handleRowClick(e);
|
|
});
|
|
|
|
// Select all checkbox
|
|
document.getElementById('check-all').addEventListener('change', (e) => {
|
|
if (e.target.checked) {
|
|
filtered.forEach((h) => checkedIps.add(String(h.ip)));
|
|
} else {
|
|
checkedIps.clear();
|
|
}
|
|
render();
|
|
});
|
|
},
|
|
getAll: () => [...data],
|
|
getUp: () => data.filter((h) => h.status === 'Up'),
|
|
getIPs: () => data.map((h) => String(h.ip)),
|
|
getUpIPs: () => data.filter((h) => h.status === 'Up').map((h) => String(h.ip)),
|
|
getChecked: () => data.filter((h) => checkedIps.has(String(h.ip))),
|
|
getCheckedIPs: () => [...checkedIps],
|
|
};
|
|
})();
|
|
const scanner = (() => {
|
|
let scanning = false;
|
|
|
|
const els = {
|
|
input: () => document.getElementById('ip-range-input'),
|
|
btnScan: () => document.getElementById('btn-scan'),
|
|
btnStop: () => document.getElementById('btn-stop'),
|
|
status: () => document.getElementById('status-text'),
|
|
bar: () => document.getElementById('progress-bar'),
|
|
fill: () => document.getElementById('progress-fill'),
|
|
};
|
|
|
|
function setScanning(active) {
|
|
scanning = active;
|
|
els.btnScan().disabled = active;
|
|
els.btnStop().disabled = !active;
|
|
els.input().disabled = active;
|
|
els.bar().hidden = !active;
|
|
if (!active) els.fill().style.width = '0%';
|
|
}
|
|
|
|
async function start() {
|
|
const range = els.input().value.trim();
|
|
if (!range) {
|
|
els.status().textContent = 'Enter an IP range';
|
|
return;
|
|
}
|
|
|
|
table.clear();
|
|
setScanning(true);
|
|
els.status().textContent = 'Scanning...';
|
|
|
|
try {
|
|
const portScan = document.getElementById('chk-portscan').checked;
|
|
await invoke('start_scan', {
|
|
config: {
|
|
range: range,
|
|
timeout_ms: 1500,
|
|
port_scan: portScan,
|
|
ports: null,
|
|
port_timeout_ms: 1000,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
els.status().textContent = 'Error: ' + err;
|
|
setScanning(false);
|
|
}
|
|
}
|
|
|
|
async function stop() {
|
|
try {
|
|
await invoke('stop_scan');
|
|
} catch (err) {
|
|
console.error('stop_scan error:', err);
|
|
}
|
|
els.status().textContent = 'Scan stopped';
|
|
setScanning(false);
|
|
}
|
|
|
|
async function setupListeners() {
|
|
await listen('scan:host-found', (e) => {
|
|
table.upsert(e.payload);
|
|
});
|
|
|
|
await listen('scan:progress', (e) => {
|
|
const p = e.payload;
|
|
const pct = Math.round((p.done / p.total) * 100);
|
|
els.fill().style.width = pct + '%';
|
|
els.status().textContent = 'Scanning... ' + p.done + ' / ' + p.total;
|
|
});
|
|
|
|
await listen('scan:complete', (e) => {
|
|
els.status().textContent = 'Scan complete (' + e.payload.total_hosts + ' hosts)';
|
|
setScanning(false);
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
els.btnScan().addEventListener('click', start);
|
|
els.btnStop().addEventListener('click', stop);
|
|
els.input().addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !scanning) start();
|
|
});
|
|
setupListeners();
|
|
}
|
|
|
|
return { init };
|
|
})();
|
|
const exporter = (() => {
|
|
async function csvAll() {
|
|
const data = table.getAll();
|
|
if (data.length === 0) return;
|
|
await saveCSV(data);
|
|
}
|
|
|
|
async function csvUp() {
|
|
const data = table.getUp();
|
|
if (data.length === 0) return;
|
|
await saveCSV(data);
|
|
}
|
|
|
|
async function saveCSV(data) {
|
|
const { save } = window.__TAURI__.dialog;
|
|
const path = await save({
|
|
defaultPath: 'argus-scan.csv',
|
|
filters: [{ name: 'CSV', extensions: ['csv'] }],
|
|
});
|
|
if (!path) return;
|
|
|
|
try {
|
|
await invoke('export_csv', { request: { path: path, hosts: data } });
|
|
} catch (err) {
|
|
console.error('Export error:', err);
|
|
}
|
|
}
|
|
|
|
return { csvAll, csvUp };
|
|
})();
|
|
const columns = (() => {
|
|
const STORAGE_KEY = 'argus-columns';
|
|
const allCols = ['ip', 'status', 'rtt', 'hostname', 'ports'];
|
|
|
|
function getVisible() {
|
|
try {
|
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
if (saved) return JSON.parse(saved);
|
|
} catch (e) { }
|
|
return [...allCols];
|
|
}
|
|
|
|
function save(visible) {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(visible));
|
|
}
|
|
|
|
function apply() {
|
|
const visible = getVisible();
|
|
allCols.forEach((col, i) => {
|
|
const show = visible.includes(col);
|
|
// Table headers
|
|
const th = document.querySelector('#results-table th[data-sort="' + col + '"]');
|
|
if (th) th.style.display = show ? '' : 'none';
|
|
// Table body cells (by column index)
|
|
const colIndex = allCols.indexOf(col) + 2;
|
|
document.querySelectorAll('#results-table tbody td:nth-child(' + colIndex + ')').forEach((td) => {
|
|
td.style.display = show ? '' : 'none';
|
|
});
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
const btn = document.getElementById('btn-columns');
|
|
const picker = document.getElementById('col-picker');
|
|
const visible = getVisible();
|
|
|
|
// Set initial checkbox state from saved prefs
|
|
picker.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
|
if (!cb.disabled) {
|
|
cb.checked = visible.includes(cb.dataset.col);
|
|
}
|
|
});
|
|
|
|
// Toggle picker
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
picker.hidden = !picker.hidden;
|
|
});
|
|
|
|
// Close picker when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!picker.contains(e.target) && e.target !== btn) {
|
|
picker.hidden = true;
|
|
}
|
|
});
|
|
|
|
// Handle checkbox changes
|
|
picker.addEventListener('change', (e) => {
|
|
const cb = e.target;
|
|
if (!cb.dataset.col) return;
|
|
|
|
const current = getVisible();
|
|
if (cb.checked) {
|
|
if (!current.includes(cb.dataset.col)) current.push(cb.dataset.col);
|
|
} else {
|
|
const idx = current.indexOf(cb.dataset.col);
|
|
if (idx >= 0) current.splice(idx, 1);
|
|
}
|
|
save(current);
|
|
apply();
|
|
});
|
|
|
|
// Apply saved preferences on load
|
|
apply();
|
|
}
|
|
|
|
return { init, apply };
|
|
})();
|
|
const sender = (() => {
|
|
function init() {
|
|
const menus = [
|
|
{ btn: 'btn-send-nmap', menu: 'send-nmap-menu' },
|
|
{ btn: 'btn-send-monitor', menu: 'send-monitor-menu' },
|
|
];
|
|
|
|
menus.forEach(({ btn, menu }) => {
|
|
const btnEl = document.getElementById(btn);
|
|
const menuEl = document.getElementById(menu);
|
|
|
|
btnEl.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
// Close any other open send menus
|
|
document.querySelectorAll('.send-menu').forEach((m) => {
|
|
if (m !== menuEl) m.hidden = true;
|
|
});
|
|
menuEl.hidden = !menuEl.hidden;
|
|
});
|
|
});
|
|
|
|
// Close menus on outside click
|
|
document.addEventListener('click', () => {
|
|
document.querySelectorAll('.send-menu').forEach((m) => m.hidden = true);
|
|
});
|
|
|
|
// Handle send actions
|
|
document.querySelectorAll('.send-menu .ctx-item').forEach((item) => {
|
|
item.addEventListener('click', (e) => {
|
|
const action = e.target.dataset.send;
|
|
let hosts;
|
|
|
|
if (action.endsWith('-all')) hosts = table.getAll();
|
|
else if (action.endsWith('-up')) hosts = table.getUp();
|
|
else if (action.endsWith('-checked')) hosts = table.getChecked();
|
|
|
|
if (!hosts || hosts.length === 0) {
|
|
console.log('No hosts to send');
|
|
return;
|
|
}
|
|
|
|
if (action.startsWith('nmap')) {
|
|
nmapTab.setTarget(hosts.map((h) => String(h.ip)));
|
|
} else if (action.startsWith('monitor')) {
|
|
monitorTab.setTargets(hosts);
|
|
}
|
|
|
|
document.querySelectorAll('.send-menu').forEach((m) => m.hidden = true);
|
|
});
|
|
});
|
|
}
|
|
|
|
return { init };
|
|
})();
|
|
const ctxMenu = (() => {
|
|
const menu = () => document.getElementById('context-menu');
|
|
let targetHost = null;
|
|
|
|
function show(e, host) {
|
|
e.preventDefault();
|
|
targetHost = host;
|
|
// Show/hide HTTP/HTTPS based on open ports
|
|
const openPorts = (host.ports || [])
|
|
.filter((p) => p.state === 'Open')
|
|
.map((p) => p.port);
|
|
const hasHttp = openPorts.some((p) => [80, 8080, 8088].includes(p));
|
|
const hasHttps = openPorts.some((p) => [443, 8443].includes(p));
|
|
menu().querySelector('[data-action="open-http"]').style.display = hasHttp ? '' : 'none';
|
|
menu().querySelector('[data-action="open-https"]').style.display = hasHttps ? '' : 'none';
|
|
const el = menu();
|
|
el.hidden = false;
|
|
|
|
// Position near the cursor, but keep it on screen
|
|
let x = e.clientX;
|
|
let y = e.clientY;
|
|
const rect = el.getBoundingClientRect();
|
|
if (x + rect.width > window.innerWidth) x = window.innerWidth - rect.width - 4;
|
|
if (y + rect.height > window.innerHeight) y = window.innerHeight - rect.height - 4;
|
|
|
|
el.style.left = x + 'px';
|
|
el.style.top = y + 'px';
|
|
}
|
|
|
|
function hide() {
|
|
menu().hidden = true;
|
|
targetHost = null;
|
|
}
|
|
|
|
async function handleAction(action) {
|
|
if (!targetHost) return;
|
|
const ip = String(targetHost.ip);
|
|
const hostname = targetHost.hostname || '';
|
|
|
|
switch (action) {
|
|
case 'copy-ip':
|
|
await navigator.clipboard.writeText(ip);
|
|
break;
|
|
case 'copy-hostname':
|
|
await navigator.clipboard.writeText(hostname || ip);
|
|
break;
|
|
case 'open-http':
|
|
await invoke('launch_url', { url: 'http://' + ip });
|
|
break;
|
|
case 'open-https':
|
|
await invoke('launch_url', { url: 'https://' + ip });
|
|
break;
|
|
case 'traceroute':
|
|
traceroute.run(ip);
|
|
break;
|
|
case 'send-monitor':
|
|
var monHost = table.getAll().find(function (h) { return String(h.ip) === ip; });
|
|
monitorTab.setTargets(monHost ? [monHost] : [ip]);
|
|
break;
|
|
case 'send-nmap':
|
|
nmapTab.setTarget([ip]);
|
|
break;
|
|
}
|
|
|
|
hide();
|
|
}
|
|
|
|
function init() {
|
|
// Hide on any click outside
|
|
document.addEventListener('click', hide);
|
|
|
|
// Handle menu item clicks
|
|
menu().addEventListener('click', (e) => {
|
|
const item = e.target.closest('.ctx-item');
|
|
if (item) handleAction(item.dataset.action);
|
|
});
|
|
|
|
// Right-click on table rows
|
|
document.getElementById('results-body').addEventListener('contextmenu', (e) => {
|
|
const row = e.target.closest('tr');
|
|
if (!row || !row.dataset.ip) return;
|
|
const host = table.getAll().find((h) => String(h.ip) === row.dataset.ip);
|
|
if (host) show(e, host);
|
|
});
|
|
}
|
|
|
|
return { init };
|
|
})();
|
|
|
|
const traceroute = (() => {
|
|
const modal = () => document.getElementById('traceroute-modal');
|
|
const output = () => document.getElementById('traceroute-output');
|
|
const target = () => document.getElementById('traceroute-target');
|
|
let running = false;
|
|
|
|
async function run(ip) {
|
|
modal().classList.add('visible');
|
|
output().innerHTML = '';
|
|
output().classList.add('traceroute-running');
|
|
target().textContent = ip;
|
|
running = true;
|
|
|
|
try {
|
|
await invoke('run_traceroute', { ip: ip });
|
|
} catch (err) {
|
|
appendLine('Error: ' + err);
|
|
}
|
|
}
|
|
|
|
function appendLine(text) {
|
|
const el = output();
|
|
const line = document.createElement('div');
|
|
line.className = 'traceroute-line';
|
|
if (text.includes('reached')) line.classList.add('hop-reached');
|
|
line.textContent = text;
|
|
el.appendChild(line);
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
function close() {
|
|
modal().classList.remove('visible');
|
|
output().classList.remove('traceroute-running');
|
|
running = false;
|
|
}
|
|
|
|
async function setupListeners() {
|
|
await listen('traceroute:hop', (e) => {
|
|
appendLine(e.payload);
|
|
});
|
|
|
|
await listen('traceroute:complete', () => {
|
|
output().classList.remove('traceroute-running');
|
|
running = false;
|
|
});
|
|
|
|
await listen('traceroute:error', (e) => {
|
|
appendLine('Error: ' + e.payload);
|
|
output().classList.remove('traceroute-running');
|
|
running = false;
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
document.getElementById('traceroute-close').addEventListener('click', close);
|
|
|
|
// Close on overlay click
|
|
modal().addEventListener('click', (e) => {
|
|
if (e.key === 'Escape' && !modal().hidden) close();
|
|
});
|
|
|
|
// Close on Escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && !modal().hidden) close();
|
|
});
|
|
|
|
setupListeners();
|
|
}
|
|
|
|
return { init, run };
|
|
})();
|
|
const nmapTab = (() => {
|
|
let running = false;
|
|
|
|
const els = {
|
|
target: () => document.getElementById('nmap-target'),
|
|
preset: () => document.getElementById('nmap-preset'),
|
|
custom: () => document.getElementById('nmap-custom-args'),
|
|
btnScan: () => document.getElementById('btn-nmap-scan'),
|
|
btnStop: () => document.getElementById('btn-nmap-stop'),
|
|
output: () => document.getElementById('nmap-output'),
|
|
};
|
|
|
|
function getArgs() {
|
|
const preset = els.preset().value;
|
|
switch (preset) {
|
|
case 'quick': return ['-sT', '-T4'];
|
|
case 'service': return ['-sT', '-sV'];
|
|
case 'os': return ['-sT', '-O'];
|
|
case 'full': return ['-sT', '-sV', '-sC'];
|
|
case 'custom':
|
|
return els.custom().value.trim().split(/\s+/).filter(Boolean);
|
|
default: return ['-sT'];
|
|
}
|
|
}
|
|
|
|
function setRunning(active) {
|
|
try {
|
|
running = active;
|
|
els.btnScan().disabled = active;
|
|
els.btnStop().disabled = !active;
|
|
els.target().disabled = active;
|
|
els.preset().disabled = active;
|
|
|
|
if (active) {
|
|
els.output().classList.add('nmap-running');
|
|
} else {
|
|
els.output().classList.remove('nmap-running');
|
|
}
|
|
|
|
var nmapTabEl = document.querySelector('[data-tab="nmap"]');
|
|
console.log('found tab:', nmapTabEl);
|
|
if (active) {
|
|
nmapTabEl.classList.add('tab-running');
|
|
} else {
|
|
nmapTabEl.classList.remove('tab-running');
|
|
}
|
|
console.log('tab class now:', nmapTabEl.classList.toString());
|
|
} catch (err) {
|
|
console.error('setRunning error:', err);
|
|
}
|
|
}
|
|
|
|
function appendLine(text) {
|
|
const el = els.output();
|
|
const line = document.createElement('div');
|
|
line.className = 'nmap-line';
|
|
|
|
if (text.match(/^\d+\/tcp\s+open/)) {
|
|
line.classList.add('nmap-line-open');
|
|
} else if (text.match(/^\d+\/tcp\s+closed/)) {
|
|
line.classList.add('nmap-line-closed');
|
|
} else if (text.startsWith('Nmap scan report') || text.startsWith('PORT')) {
|
|
line.classList.add('nmap-line-header');
|
|
} else if (text.startsWith('|') || text.startsWith('Service Info')) {
|
|
line.classList.add('nmap-line-info');
|
|
}
|
|
|
|
line.textContent = text;
|
|
el.appendChild(line);
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
async function start() {
|
|
const target = els.target().value.trim();
|
|
if (!target) return;
|
|
|
|
els.output().innerHTML = '';
|
|
setRunning(true);
|
|
|
|
try {
|
|
const args = getArgs();
|
|
if (document.getElementById('chk-nmap-verbose').checked) {
|
|
args.unshift('-v');
|
|
}
|
|
await invoke('run_nmap', {
|
|
config: { target: target, args: args },
|
|
});
|
|
} catch (err) {
|
|
appendLine('Error: ' + err);
|
|
setRunning(false);
|
|
}
|
|
}
|
|
|
|
async function stop() {
|
|
try {
|
|
await invoke('stop_nmap');
|
|
} catch (err) {
|
|
console.error('stop_nmap error:', err);
|
|
}
|
|
}
|
|
|
|
function setTarget(ips) {
|
|
els.target().value = ips.join(' ');
|
|
// Switch to nmap tab
|
|
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
|
|
document.querySelectorAll('.view').forEach((v) => v.classList.remove('active'));
|
|
document.querySelector('[data-tab="nmap"]').classList.add('active');
|
|
document.getElementById('view-nmap').classList.add('active');
|
|
}
|
|
|
|
async function setupListeners() {
|
|
await listen('nmap:line', (e) => {
|
|
appendLine(e.payload);
|
|
});
|
|
|
|
await listen('nmap:complete', (e) => {
|
|
appendLine('');
|
|
appendLine(e.payload);
|
|
setRunning(false);
|
|
});
|
|
|
|
await listen('nmap:error', (e) => {
|
|
appendLine('Error: ' + e.payload);
|
|
setRunning(false);
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
els.btnScan().addEventListener('click', start);
|
|
els.btnStop().addEventListener('click', stop);
|
|
|
|
els.target().addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !running) start();
|
|
});
|
|
|
|
// Show/hide custom args field
|
|
els.preset().addEventListener('change', () => {
|
|
els.custom().hidden = els.preset().value !== 'custom';
|
|
});
|
|
|
|
setupListeners();
|
|
}
|
|
|
|
return { init, setTarget };
|
|
})();
|
|
const monitorTab = (() => {
|
|
let running = false;
|
|
let entries = {};
|
|
var editingLabel = false;
|
|
|
|
var els = {
|
|
input: () => document.getElementById('monitor-input'),
|
|
interval: () => document.getElementById('monitor-interval'),
|
|
btnAdd: () => document.getElementById('btn-monitor-add'),
|
|
btnStart: () => document.getElementById('btn-monitor-start'),
|
|
btnStop: () => document.getElementById('btn-monitor-stop'),
|
|
btnClear: () => document.getElementById('btn-monitor-clear'),
|
|
body: () => document.getElementById('monitor-body'),
|
|
};
|
|
|
|
function setRunning(active) {
|
|
running = active;
|
|
els.btnStart().disabled = active;
|
|
els.btnStop().disabled = !active;
|
|
els.interval().disabled = active;
|
|
|
|
var monTabEl = document.querySelector('[data-tab="monitor"]');
|
|
if (active) {
|
|
monTabEl.classList.add('tab-running');
|
|
} else {
|
|
monTabEl.classList.remove('tab-running');
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
if (editingLabel) return;
|
|
var body = els.body();
|
|
var ips = Object.keys(entries).sort();
|
|
|
|
var rows = ips.map(function (ip) {
|
|
var e = entries[ip];
|
|
var statusClass = 'monitor-status-' + e.status;
|
|
var lastRtt = e.last_rtt_ms !== null ? e.last_rtt_ms.toFixed(1) : '-';
|
|
var minRtt = e.min_rtt_ms !== null ? e.min_rtt_ms.toFixed(1) : '-';
|
|
var avgRtt = e.avg_rtt_ms !== null ? e.avg_rtt_ms.toFixed(1) : '-';
|
|
var maxRtt = e.max_rtt_ms !== null ? e.max_rtt_ms.toFixed(1) : '-';
|
|
var loss = e.loss_pct.toFixed(1);
|
|
var label = e.label || '-';
|
|
|
|
return '<tr data-ip="' + ip + '">'
|
|
+ '<td>' + ip + '</td>'
|
|
+ '<td class="editable-label" data-ip="' + ip + '">' + label + '</td>'
|
|
+ '<td class="' + statusClass + '">' + e.status + '</td>'
|
|
+ '<td>' + lastRtt + '</td>'
|
|
+ '<td>' + minRtt + '</td>'
|
|
+ '<td>' + avgRtt + '</td>'
|
|
+ '<td>' + maxRtt + '</td>'
|
|
+ '<td>' + e.packets_sent + '</td>'
|
|
+ '<td>' + e.packets_received + '</td>'
|
|
+ '<td>' + loss + '</td>'
|
|
+ '<td>' + e.consecutive_failures + '</td>'
|
|
+ '<td><button class="btn-remove" data-ip="' + ip + '">×</button></td>'
|
|
+ '</tr>';
|
|
});
|
|
|
|
body.innerHTML = rows.join('');
|
|
}
|
|
|
|
async function addHosts() {
|
|
var input = els.input().value.trim();
|
|
if (!input) return;
|
|
|
|
var hosts = input.split(',').map(function (s) {
|
|
return { ip: s.trim(), label: null };
|
|
}).filter(function (h) { return h.ip.length > 0; });
|
|
|
|
try {
|
|
await invoke('monitor_add_hosts', { hosts: hosts });
|
|
|
|
for (var i = 0; i < hosts.length; i++) {
|
|
try {
|
|
var hostname = await invoke('monitor_resolve_host', { ip: hosts[i].ip });
|
|
if (hostname) {
|
|
await invoke('monitor_update_label', { ip: hosts[i].ip, label: hostname });
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
|
|
var updated = await invoke('monitor_get_entries');
|
|
entries = {};
|
|
updated.forEach(function (e) { entries[e.ip] = e; });
|
|
render();
|
|
els.input().value = '';
|
|
} catch (err) {
|
|
console.error('Failed to add hosts:', err);
|
|
}
|
|
}
|
|
|
|
async function removeHost(ip) {
|
|
try {
|
|
await invoke('monitor_remove_host', { ip: ip });
|
|
delete entries[ip];
|
|
render();
|
|
} catch (err) {
|
|
console.error('Failed to remove host:', err);
|
|
}
|
|
}
|
|
|
|
async function start() {
|
|
if (Object.keys(entries).length === 0) return;
|
|
|
|
var interval = parseInt(els.interval().value, 10);
|
|
setRunning(true);
|
|
|
|
try {
|
|
await invoke('monitor_start', { intervalMs: interval });
|
|
} catch (err) {
|
|
console.error('Monitor start error:', err);
|
|
setRunning(false);
|
|
}
|
|
}
|
|
|
|
async function stop() {
|
|
try {
|
|
await invoke('monitor_stop');
|
|
} catch (err) {
|
|
console.error('Monitor stop error:', err);
|
|
}
|
|
setRunning(false);
|
|
}
|
|
|
|
async function clearAll() {
|
|
try {
|
|
if (running) await stop();
|
|
await invoke('monitor_clear');
|
|
entries = {};
|
|
render();
|
|
} catch (err) {
|
|
console.error('Monitor clear error:', err);
|
|
}
|
|
}
|
|
|
|
function setTargets(hosts) {
|
|
var items = hosts.map(function (h) {
|
|
if (typeof h === 'string') return { ip: h, label: null };
|
|
return { ip: String(h.ip), label: h.hostname || null };
|
|
});
|
|
|
|
invoke('monitor_add_hosts', { hosts: items }).then(function () {
|
|
return invoke('monitor_get_entries');
|
|
}).then(function (updated) {
|
|
entries = {};
|
|
updated.forEach(function (e) { entries[e.ip] = e; });
|
|
render();
|
|
});
|
|
|
|
document.querySelectorAll('.tab').forEach(function (t) { t.classList.remove('active'); });
|
|
document.querySelectorAll('.view').forEach(function (v) { v.classList.remove('active'); });
|
|
document.querySelector('[data-tab="monitor"]').classList.add('active');
|
|
document.getElementById('view-monitor').classList.add('active');
|
|
}
|
|
|
|
async function setupListeners() {
|
|
await listen('monitor:update', function (e) {
|
|
var entry = e.payload;
|
|
entries[entry.ip] = entry;
|
|
render();
|
|
});
|
|
|
|
await listen('monitor:state-change', function (e) {
|
|
var data = e.payload;
|
|
console.log('State change:', data.ip, data.old, '->', data.new);
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
els.btnAdd().addEventListener('click', addHosts);
|
|
els.btnStart().addEventListener('click', start);
|
|
els.btnStop().addEventListener('click', stop);
|
|
els.btnClear().addEventListener('click', clearAll);
|
|
|
|
els.input().addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') addHosts();
|
|
});
|
|
|
|
// Remove button delegation
|
|
els.body().addEventListener('click', function (e) {
|
|
var btn = e.target.closest('.btn-remove');
|
|
if (btn) removeHost(btn.dataset.ip);
|
|
});
|
|
els.body().addEventListener('dblclick', function (e) {
|
|
var cell = e.target.closest('.editable-label');
|
|
if (!cell) return;
|
|
var ip = cell.dataset.ip;
|
|
var currentLabel = cell.textContent === '-' ? '' : cell.textContent;
|
|
editingLabel = true;
|
|
var input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = currentLabel;
|
|
input.className = 'label-edit-input';
|
|
cell.textContent = '';
|
|
cell.appendChild(input);
|
|
input.focus();
|
|
input.select();
|
|
|
|
function save() {
|
|
var newLabel = input.value.trim();
|
|
editingLabel = false;
|
|
invoke('monitor_update_label', { ip: ip, label: newLabel }).then(function () {
|
|
return invoke('monitor_get_entries');
|
|
}).then(function (updated) {
|
|
entries = {};
|
|
updated.forEach(function (e) { entries[e.ip] = e; });
|
|
render();
|
|
});
|
|
}
|
|
|
|
input.addEventListener('blur', save);
|
|
input.addEventListener('keydown', function (ev) {
|
|
if (ev.key === 'Enter') save();
|
|
if (ev.key === 'Escape')
|
|
editingLabel = false;
|
|
render();
|
|
});
|
|
});
|
|
|
|
setupListeners();
|
|
}
|
|
|
|
return { init, setTargets };
|
|
})();
|
|
var updater = (() => {
|
|
function showBanner(info) {
|
|
var banner = document.getElementById('update-banner');
|
|
var msg = document.getElementById('update-message');
|
|
var link = document.getElementById('update-link');
|
|
var text = 'Argus v' + info.version + ' is available.';
|
|
msg.textContent = text;
|
|
link.href = info.url;
|
|
link.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
invoke('launch_url', { url: info.url });
|
|
});
|
|
banner.classList.add('visible');
|
|
}
|
|
|
|
async function check() {
|
|
try {
|
|
var update = await invoke('check_for_update');
|
|
if (update) showBanner(update);
|
|
} catch (err) {
|
|
console.log('Update check skipped:', err);
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
document.getElementById('update-dismiss').addEventListener('click', function () {
|
|
document.getElementById('update-banner').classList.remove('visible');
|
|
});
|
|
|
|
// Check on startup after a short delay
|
|
setTimeout(check, 3000);
|
|
}
|
|
|
|
return { init };
|
|
})();
|
|
var changelog = (() => {
|
|
var modal = function () { return document.getElementById('changelog-modal'); };
|
|
var loaded = false;
|
|
|
|
async function open() {
|
|
modal().classList.add('visible');
|
|
if (!loaded) {
|
|
try {
|
|
var html = await invoke('get_changelog');
|
|
document.getElementById('changelog-content').innerHTML = html;
|
|
loaded = true;
|
|
} catch (err) {
|
|
document.getElementById('changelog-content').textContent = 'Failed to load changelog.';
|
|
}
|
|
}
|
|
}
|
|
|
|
function close() {
|
|
modal().classList.remove('visible');
|
|
}
|
|
|
|
function init() {
|
|
document.getElementById('btn-changelog').addEventListener('click', open);
|
|
document.getElementById('changelog-close').addEventListener('click', close);
|
|
|
|
modal().addEventListener('click', function (e) {
|
|
if (e.target === modal()) close();
|
|
});
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape' && modal().classList.contains('visible')) close();
|
|
});
|
|
}
|
|
|
|
return { init };
|
|
})();
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
// Tab switching
|
|
document.querySelectorAll('.tab').forEach((tab) => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
|
|
document.querySelectorAll('.view').forEach((v) => v.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
const view = document.getElementById('view-' + tab.dataset.tab);
|
|
if (view) view.classList.add('active');
|
|
});
|
|
});
|
|
|
|
// Sort on header click
|
|
document.querySelectorAll('#results-table th.sortable').forEach((th) => {
|
|
th.addEventListener('click', () => table.setSort(th.dataset.sort));
|
|
});
|
|
|
|
// Filter input
|
|
document.getElementById('filter-input').addEventListener('input', (e) => {
|
|
table.setFilter(e.target.value);
|
|
});
|
|
|
|
// Boot scanner
|
|
table.init();
|
|
scanner.init();
|
|
ctxMenu.init();
|
|
traceroute.init();
|
|
columns.init();
|
|
sender.init();
|
|
nmapTab.init();
|
|
monitorTab.init();
|
|
updater.init();
|
|
changelog.init();
|
|
theme.init();
|
|
// Always on top toggle
|
|
document.getElementById('chk-always-on-top').addEventListener('change', function (e) {
|
|
if (window.__TAURI__ && window.__TAURI__.window) {
|
|
var current = window.__TAURI__.window.getCurrentWindow();
|
|
current.setAlwaysOnTop(e.target.checked);
|
|
}
|
|
});
|
|
document.getElementById('btn-export-all').addEventListener('click', exporter.csvAll);
|
|
document.getElementById('btn-export-up').addEventListener('click', exporter.csvUp);
|
|
console.log('[Argus] Ready');
|
|
// Set version from Tauri config
|
|
if (window.__TAURI__ && window.__TAURI__.app) {
|
|
window.__TAURI__.app.getVersion().then(function (version) {
|
|
document.getElementById('status-bar-version').textContent = 'Argus v' + version;
|
|
});
|
|
}
|
|
// Check privilege status
|
|
invoke('check_privileges').then(function (elevated) {
|
|
var el = document.getElementById('privilege-status');
|
|
if (elevated) {
|
|
el.textContent = 'elevated';
|
|
el.classList.add('privilege-elevated');
|
|
} else {
|
|
el.textContent = 'unprivileged';
|
|
}
|
|
});
|
|
}); |