diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a7f2e..4c73be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [0.3.0] - 2026-10-08 + +### πŸš€ Features + +- Added Auto Labeling to Montior Tab. When sending hosts from the scan tab, it will bring their hostnames over to the label section. When adding a one off host to the montior tab, it will auto query it's hostname. You can also edit labels by double clicking. Closes #3 +- Added Always on Top Toggle. Closes #5 +- Added New Theme + +### πŸ› Bug Fixes + +- Status Colors on Monitor tab now reflect status of host. Closes #7 +- Removed unused function. +- Fixed Screen Refresh Bug on Monitor Tab. Now When You Go To Change a Label While the Monitor Is Running. The Page Stops Refreshing Until You’re Done Changing the Name. This Is To Prevent Your Text Box From Being Deselected When the Table Refreshes. ## [0.2.0] - 2026-09-24 ### πŸš€ Features diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ee13966..1f4f06a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -49,7 +49,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "argus" -version = "0.2.0" +version = "0.3.0" dependencies = [ "csv", "dns-lookup", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 07a3276..a904673 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "argus" -version = "0.2.0" +version = "0.3.0" description = "An All in One Network Utility" authors = ["James Brotosky Result<(), String> { } Ok(()) } +#[tauri::command] +pub async fn monitor_update_label(ip: String, label: String) -> Result<(), String> { + let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?; + let state = get_monitor(); + let mut guard = state.lock().await; + if let Some(entry) = guard.entries.get_mut(&parsed) { + entry.label = if label.is_empty() { None } else { Some(label) }; + } + Ok(()) +} + +#[tauri::command] +pub async fn monitor_resolve_host(ip: String) -> Result, String> { + let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?; + Ok(crate::net::dns::reverse_lookup(parsed).await) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8a6c46c..43bb668 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -32,6 +32,8 @@ pub fn run() { commands::updater::check_for_update, commands::actions::check_privileges, commands::changelog::get_changelog, + commands::monitor::monitor_update_label, + commands::monitor::monitor_resolve_host ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/net/dns.rs b/src-tauri/src/net/dns.rs index 189d520..29e0169 100644 --- a/src-tauri/src/net/dns.rs +++ b/src-tauri/src/net/dns.rs @@ -1,5 +1,4 @@ use std::net::IpAddr; -use tokio::task::JoinSet; /// Perform reverse DNS lookup on a single IP. /// Returns the hostname if found, None otherwise. @@ -18,33 +17,4 @@ pub async fn reverse_lookup(ip: IpAddr) -> Option { } _ => None, } -} - -/// Perform reverse DNS lookups on a batch of IPs concurrently. -/// Returns a Vec of (IpAddr, Option) pairs. -pub async fn reverse_lookup_batch( - ips: &[IpAddr], - max_concurrent: usize, -) -> Vec<(IpAddr, Option)> { - let mut results = Vec::with_capacity(ips.len()); - - for chunk in ips.chunks(max_concurrent) { - let mut join_set = JoinSet::new(); - - for &ip in chunk { - join_set.spawn(async move { - let hostname = reverse_lookup(ip).await; - (ip, hostname) - }); - } - - while let Some(result) = join_set.join_next().await { - match result { - Ok(pair) => results.push(pair), - Err(e) => tracing::warn!("DNS lookup task panicked: {}", e), - } - } - } - - results } \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4848268..20aeb8b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "argus", - "version": "0.2.0", + "version": "0.3.0", "identifier": "io.argus.app", "build": { "frontendDist": "../src" diff --git a/src/index.html b/src/index.html index 2a97886..2e2d141 100644 --- a/src/index.html +++ b/src/index.html @@ -200,12 +200,17 @@
Argus + unprivileged
@@ -243,7 +248,7 @@ - +
diff --git a/src/main.js b/src/main.js index 6827aff..864a02c 100644 --- a/src/main.js +++ b/src/main.js @@ -22,15 +22,63 @@ var theme = (() => { } 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 @@ -609,7 +657,7 @@ const sender = (() => { if (action.startsWith('nmap')) { nmapTab.setTarget(hosts.map((h) => String(h.ip))); } else if (action.startsWith('monitor')) { - monitorTab.setTargets(hosts.map(function (h) { return String(h.ip); })); + monitorTab.setTargets(hosts); } document.querySelectorAll('.send-menu').forEach((m) => m.hidden = true); @@ -675,7 +723,8 @@ const ctxMenu = (() => { traceroute.run(ip); break; case 'send-monitor': - monitorTab.setTargets([ip]); + var monHost = table.getAll().find(function (h) { return String(h.ip) === ip; }); + monitorTab.setTargets(monHost ? [monHost] : [ip]); break; case 'send-nmap': nmapTab.setTarget([ip]); @@ -926,6 +975,7 @@ const nmapTab = (() => { const monitorTab = (() => { let running = false; let entries = {}; + var editingLabel = false; var els = { input: () => document.getElementById('monitor-input'), @@ -952,6 +1002,7 @@ const monitorTab = (() => { } function render() { + if (editingLabel) return; var body = els.body(); var ips = Object.keys(entries).sort(); @@ -967,7 +1018,7 @@ const monitorTab = (() => { return '' + '' + ip + '' - + '' + label + '' + + '' + label + '' + '' + e.status + '' + '' + lastRtt + '' + '' + minRtt + '' @@ -994,6 +1045,16 @@ const monitorTab = (() => { 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; }); @@ -1048,9 +1109,20 @@ const monitorTab = (() => { } } - function setTargets(ips) { - els.input().value = ips.join(', '); - // Switch to monitor tab + 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'); @@ -1085,6 +1157,41 @@ const monitorTab = (() => { 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(); } @@ -1196,6 +1303,13 @@ window.addEventListener('DOMContentLoaded', () => { 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'); diff --git a/src/styles.css b/src/styles.css index 5cc067f..cca5fb1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1146,17 +1146,17 @@ body { text-shadow: 0 0 6px var(--status-up-glow); } -.monitor-status-Down { +#monitor-table .monitor-status-Down { color: var(--status-down); text-shadow: 0 0 6px var(--status-down-glow); } -.monitor-status-Degraded { +#monitor-table .monitor-status-Degraded { color: var(--status-filtered); text-shadow: 0 0 6px var(--status-filtered-glow); } -.monitor-status-Unknown { +#monitor-table .monitor-status-Unknown { color: var(--status-unknown); } @@ -1478,6 +1478,174 @@ body { --danger-hover: #c62825; } +/* ===== Theme: Catppuccin Mocha ===== */ + +:root[data-theme="mocha"] { + --bg-primary: #1e1e2e; + --bg-secondary: #181825; + --bg-tertiary: #313244; + --bg-hover: #45475a; + --bg-active: #585b70; + --border: #313244; + --border-bright: #45475a; + + --text-primary: #cdd6f4; + --text-secondary: #a6adc8; + --text-muted: #6c7086; + + --cyan: #89b4fa; + --cyan-hover: #7ba4eb; + --cyan-dim: rgba(137, 180, 250, 0.08); + --cyan-glow: rgba(137, 180, 250, 0.2); + --cyan-subtle: rgba(137, 180, 250, 0.03); + + --status-up: #a6e3a1; + --status-up-glow: rgba(166, 227, 161, 0.15); + --status-down: #f38ba8; + --status-down-glow: rgba(243, 139, 168, 0.15); + --status-filtered: #f9e2af; + --status-filtered-glow: rgba(249, 226, 175, 0.15); + --status-unknown: #6c7086; + + --danger: #f38ba8; + --danger-hover: #e57a97; +} + +:root[data-theme="mocha"] .toolbar-circuit { + filter: hue-rotate(40deg) saturate(0.6) brightness(0.9); +} + +:root[data-theme="latte"] .port-tag { + background: rgba(30, 102, 245, 0.1); + border-color: rgba(30, 102, 245, 0.2); +} + +:root[data-theme="latte"] #privilege-status::before { + background: var(--status-up); + box-shadow: 0 0 4px rgba(64, 160, 43, 0.3); +} + +:root[data-theme="latte"] ::-webkit-scrollbar-track { + background: var(--bg-primary); +} + +:root[data-theme="latte"] ::-webkit-scrollbar-thumb { + background: var(--border-bright); +} + +/* ===== Theme: Hacker (Easter Egg) ===== */ + +:root[data-theme="hacker"] { + --bg-primary: #000000; + --bg-secondary: #0a0a0a; + --bg-tertiary: #0d0d0d; + --bg-hover: #1a1a1a; + --bg-active: #222222; + --border: #0a3a0a; + --border-bright: #0f4f0f; + + --text-primary: #00ff41; + --text-secondary: #00cc33; + --text-muted: #006b1d; + + --cyan: #00ff41; + --cyan-hover: #00dd38; + --cyan-dim: rgba(0, 255, 65, 0.06); + --cyan-glow: rgba(0, 255, 65, 0.3); + --cyan-subtle: rgba(0, 255, 65, 0.02); + + --status-up: #00ff41; + --status-up-glow: rgba(0, 255, 65, 0.2); + --status-down: #ff0040; + --status-down-glow: rgba(255, 0, 64, 0.2); + --status-filtered: #ffff00; + --status-filtered-glow: rgba(255, 255, 0, 0.15); + --status-unknown: #006b1d; + + --danger: #ff0040; + --danger-hover: #dd0038; + + --font-mono: "Courier New", "Lucida Console", monospace; + --font-sans: "Courier New", "Lucida Console", monospace; +} + +:root[data-theme="hacker"] .toolbar-circuit { + filter: hue-rotate(-60deg) saturate(3) brightness(0.8); +} + +:root[data-theme="hacker"] body { + font-family: "Courier New", "Lucida Console", monospace; +} + +:root[data-theme="hacker"] .app-title { + letter-spacing: 5px; +} + +:root[data-theme="hacker"] .btn-primary { + text-shadow: 0 0 8px rgba(0, 255, 65, 0.5); +} + +:root[data-theme="hacker"] #results-table td, +:root[data-theme="hacker"] #monitor-table td { + text-shadow: 0 0 3px rgba(0, 255, 65, 0.15); +} + +#crt-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 9999; +} + +:root[data-theme="hacker"] #crt-overlay { + display: block; + background: + repeating-linear-gradient(0deg, + rgba(0, 0, 0, 0.4) 0px, + rgba(0, 0, 0, 0.4) 1px, + transparent 1px, + transparent 4px), + linear-gradient(180deg, + transparent 0%, + rgba(0, 255, 65, 0.03) 50%, + transparent 100%); + animation: crt-flicker 3s infinite; +} + +@keyframes crt-flicker { + 0% { + opacity: 1; + } + + 5% { + opacity: 0.85; + } + + 10% { + opacity: 1; + } + + 40% { + opacity: 0.95; + } + + 45% { + opacity: 1; + } + + 80% { + opacity: 0.9; + } + + 85% { + opacity: 1; + } +} + /* ===== Circuit art theme tinting ===== */ :root[data-theme="dracula"] .toolbar-circuit { @@ -1494,4 +1662,33 @@ body { :root[data-theme="midnight"] .toolbar-circuit { filter: brightness(0.8); +} + +.editable-label { + cursor: pointer; +} + +.editable-label:hover { + background: var(--cyan-dim); +} + +.label-edit-input { + width: 100%; + padding: 2px 6px; + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-primary); + background: var(--bg-primary); + border: 1px solid var(--cyan); + border-radius: var(--radius); + outline: none; +} + +.status-bar-toggle { + font-size: 11px; + color: var(--text-muted); +} + +.status-bar-toggle input:checked+span { + color: var(--cyan); } \ No newline at end of file