From 8f7f7eaed75e0d178266d9889f23d3a505c1c0bc Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Sun, 10 May 2026 08:45:52 +1000 Subject: [PATCH 01/12] BE: Trigger failing if object non-existent + emoji removal in upgrade message Signed-off-by: jokob-sk --- server/initialise.py | 4 +- server/workflows/actions.py | 128 +++++++++++++++++++++-------------- server/workflows/triggers.py | 6 +- 3 files changed, 83 insertions(+), 55 deletions(-) diff --git a/server/initialise.py b/server/initialise.py index 2414aa080..9509b06bb 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -724,13 +724,13 @@ def importConfigs(pm, db, all_plugins): write_notification( f"""[Upgrade]: App upgraded from {prev_version} to \ - {new_version} 🚀 Please clear the cache: \ + {new_version} Please clear the cache: \
  1. Click OK below
  2. \
  3. Clear the browser cache (shift + browser refresh button)
  4. \
  5. Clear app cache with the (reload) button in the header
  6. \
  7. Go to Settings and click Save
\ Check out new features and what has changed in the \ - 📓 release notes.""", + release notes.""", 'interrupt', timeNowUTC() ) diff --git a/server/workflows/actions.py b/server/workflows/actions.py index 429da0f59..38edb2956 100755 --- a/server/workflows/actions.py +++ b/server/workflows/actions.py @@ -14,8 +14,16 @@ class Action: def __init__(self, trigger): self.trigger = trigger - def execute(self, obj): - """Executes the action on the given object.""" + def get_object(self): + """Safely get and normalize the trigger object.""" + obj = getattr(self.trigger, "object", None) + + if isinstance(obj, sqlite3.Row): + obj = dict(obj) + + return obj + + def execute(self): raise NotImplementedError("Subclasses must implement execute()") @@ -23,7 +31,7 @@ class UpdateFieldAction(Action): """Action to update a specific field of an object.""" def __init__(self, db, field, value, trigger): - super().__init__(trigger) # Call the base class constructor + super().__init__(trigger) self.field = field self.value = value self.db = db @@ -31,29 +39,35 @@ def __init__(self, db, field, value, trigger): def execute(self): mylog("verbose", f"[WF] Updating field '{self.field}' to '{self.value}' for event object {self.trigger.object_type}") - obj = self.trigger.object - - # convert to dict for easeir handling - if isinstance(obj, sqlite3.Row): - obj = dict(obj) # Convert Row object to a standard dictionary + obj = self.get_object() - processed = False + if obj is None: + mylog("none", "[WF] Object no longer exists") + return None - # currently unused if isinstance(obj, dict) and "objectGuid" in obj: - mylog("debug", f"[WF] Updating Object '{obj}' ") - plugin_instance = PluginObjectInstance() - plugin_instance.updateField(obj["objectGuid"], self.field, self.value) - processed = True + mylog("debug", f"[WF] Updating Object '{obj}'") + + PluginObjectInstance().updateField( + obj["objectGuid"], + self.field, + self.value, + ) + + return obj + + if isinstance(obj, dict) and "devGUID" in obj: + mylog("debug", f"[WF] Updating Device '{obj}'") - elif isinstance(obj, dict) and "devGUID" in obj: - mylog("debug", f"[WF] Updating Device '{obj}' ") - device_instance = DeviceInstance() - device_instance.updateField(obj["devGUID"], self.field, self.value) - processed = True + DeviceInstance().updateField( + obj["devGUID"], + self.field, + self.value, + ) - if not processed: - mylog("none", f"[WF] Could not process action for object: {obj}") + return obj + + mylog("none", f"[WF] Unsupported object format: {obj}") return obj @@ -62,35 +76,33 @@ class DeleteObjectAction(Action): """Action to delete an object.""" def __init__(self, db, trigger): - super().__init__(trigger) # Call the base class constructor + super().__init__(trigger) self.db = db def execute(self): mylog("verbose", f"[WF] Deleting event object {self.trigger.object_type}") - obj = self.trigger.object - - # convert to dict for easeir handling - if isinstance(obj, sqlite3.Row): - obj = dict(obj) # Convert Row object to a standard dictionary + obj = self.get_object() - processed = False + if obj is None: + mylog("none", "[WF] Object no longer exists") + return None - # currently unused if isinstance(obj, dict) and "objectGuid" in obj: - mylog("debug", f"[WF] Updating Object '{obj}' ") - plugin_instance = PluginObjectInstance() - plugin_instance.delete(obj["objectGuid"]) - processed = True + mylog("debug", f"[WF] Deleting Object '{obj}'") - elif isinstance(obj, dict) and "devGUID" in obj: - mylog("debug", f"[WF] Updating Device '{obj}' ") - device_instance = DeviceInstance() - device_instance.delete(obj["devGUID"]) - processed = True + PluginObjectInstance().delete(obj["objectGuid"]) - if not processed: - mylog("none", f"[WF] Could not process action for object: {obj}") + return obj + + if isinstance(obj, dict) and "devGUID" in obj: + mylog("debug", f"[WF] Deleting Device '{obj}'") + + DeviceInstance().delete(obj["devGUID"]) + + return obj + + mylog("none", f"[WF] Unsupported object format: {obj}") return obj @@ -98,16 +110,22 @@ def execute(self): class RunPluginAction(Action): """Action to run a specific plugin.""" - def __init__(self, plugin_name, params, trigger): # Add trigger - super().__init__(trigger) # Call parent constructor + def __init__(self, plugin_name, params, trigger): + super().__init__(trigger) self.plugin_name = plugin_name self.params = params def execute(self): - obj = self.trigger.object + obj = self.get_object() + + if obj is None: + mylog("none", "[WF] Object no longer exists") + return None + + mylog("verbose", f"[WF] Executing plugin '{self.plugin_name}' with parameters {self.params} for object {obj}") + + # PluginManager.run(self.plugin_name, self.params) - mylog("verbose", f"Executing plugin '{self.plugin_name}' with parameters {self.params} for object {obj}") - # PluginManager.run(self.plugin_name, self.parameters) return obj @@ -115,14 +133,21 @@ class SendNotificationAction(Action): """Action to send a notification.""" def __init__(self, method, message, trigger): - super().__init__(trigger) # Call parent constructor - self.method = method # Fix attribute name + super().__init__(trigger) + self.method = method self.message = message def execute(self): - obj = self.trigger.object - mylog("verbose", f"Sending notification via '{self.method}': {self.message} for object {obj}") + obj = self.get_object() + + if obj is None: + mylog("none", "[WF] Object no longer exists") + return None + + mylog("verbose", f"[WF] Sending notification via '{self.method}': {self.message} for object {obj}") + # NotificationManager.send(self.method, self.message) + return obj @@ -132,7 +157,6 @@ class ActionGroup: def __init__(self, actions): self.actions = actions - def execute(self, obj): + def execute(self): for action in self.actions: - action.execute(obj) - return obj + action.execute() \ No newline at end of file diff --git a/server/workflows/triggers.py b/server/workflows/triggers.py index ed8ec4b92..86e305ec3 100755 --- a/server/workflows/triggers.py +++ b/server/workflows/triggers.py @@ -48,7 +48,11 @@ def __init__(self, triggerJson, event, db): mylog("debug", [query]) result = db.sql.execute(query).fetchall() - self.object = result[0] + + if len(result) > 0: + self.object = result[0] + else: + self.object = None else: self.object = None From 1def218db5a0168b9a343c5d9daf256d89021626 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Sat, 9 May 2026 23:35:21 +0000 Subject: [PATCH 02/12] Update README and enhance device management logic - Updated Trendshift repository link in README.md. - Improved error handling and user feedback in deviceDetails.php: - Added warnings for device not found scenarios. - Refactored device name and owner retrieval logic with caching and REST API fallback. --- README.md | 2 +- front/deviceDetails.php | 81 +++++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ba0777a24..4b646927f 100755 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Check the [GitHub Issues](https://github.com/netalertx/NetAlertX/issues) for the ## Everything else -jokob-sk%2FNetAlertX | Trendshift +jokob-sk%2FNetAlertX | Trendshift ### 📧 Get notified what's new diff --git a/front/deviceDetails.php b/front/deviceDetails.php index 6c0a1180b..cd8de5d90 100755 --- a/front/deviceDetails.php +++ b/front/deviceDetails.php @@ -307,7 +307,10 @@ function updateChevrons(currentMac) { pos = refreshedList.findIndex(item => item.devMac === currentMac); if (pos === -1) { - console.error('Still not found after re-cache:', currentMac); + console.warn('Device not found in device list after re-cache — hiding navigation controls:', currentMac); + $('#txtRecord').hide(); + $('#btnPrevious').hide(); + $('#btnNext').hide(); return; } @@ -499,26 +502,9 @@ function initializeTabs () { } } -function updateDevicePageName(mac) { - let name = getDevDataByMac(mac, "devName"); - let owner = getDevDataByMac(mac, "devOwner"); - - // If data is missing, re-cache and retry once - if (mac != 'new' && (name === null|| owner === null)) { - console.warn("Device not found in cache, retrying after re-cache:", mac); - showSpinner(); - cacheDevices(true).then(() => { - hideSpinner(); - // Retry after successful cache - updateDevicePageName(mac); - }).catch((err) => { - hideSpinner(); - console.error("Failed to refresh devices:", err); - }); - return; // Exit early to avoid showing bad data - } - - // Page title - Name +// ---------------------------------------- +// Write device name/owner into page title DOM. Pure DOM side-effect, no data fetching. +function applyDevicePageTitle(mac, name, owner) { let pageTitleText; if (mac === "new") { @@ -530,12 +516,12 @@ function updateDevicePageName(mac) { ` ` + getString("Gen_create_new_device_info") ); $('#devicePageInfoPlc').show(); - } else if (!owner || name.toString().includes(owner)) { - pageTitleText = name; + } else if (!owner || (name && name.toString().includes(owner))) { + pageTitleText = name ?? getString("DevDetail_EveandAl_NewDevice"); $('#pageTitle').html(pageTitleText); $('#devicePageInfoPlc').hide(); } else { - pageTitleText = `${name} (${owner})`; + pageTitleText = `${name ?? getString("DevDetail_EveandAl_NewDevice")} (${owner})`; $('#pageTitle').html(pageTitleText); $('#devicePageInfoPlc').hide(); } @@ -544,6 +530,53 @@ function updateDevicePageName(mac) { $('title').html(pageTitleText + ' - ' + $('title').html()); } +// ---------------------------------------- +// Resolve device name/owner for the page title. +// Stage 1: localStorage cache (synchronous, fast path). +// Stage 2: one forced re-cache from table_devices.json. +// Stage 3: REST API fallback so a direct-link visit never loops. +async function updateDevicePageName(mac) { + let name = getDevDataByMac(mac, "devName"); + let owner = getDevDataByMac(mac, "devOwner"); + + // Stage 2: one re-cache attempt + if (mac !== 'new' && (name === null || owner === null)) { + console.warn("Device not in cache, attempting re-cache:", mac); + showSpinner(); + try { + await cacheDevices(true); + } catch (err) { + console.error("Re-cache failed:", err); + } finally { + hideSpinner(); + } + name = getDevDataByMac(mac, "devName"); + owner = getDevDataByMac(mac, "devOwner"); + } + + // Stage 3: REST fallback — same endpoint renderSmallBoxes uses, always DB-direct + if (mac !== 'new' && (name === null || owner === null)) { + console.warn("Device not found in cache after re-cache, falling back to REST API:", mac); + try { + const { apiBase, authHeader } = getAuthContext(); + const res = await fetch(`${apiBase}/device/${encodeURIComponent(mac)}`, { + headers: authHeader + }); + if (res.ok) { + const data = await res.json(); + name = data.devName ?? null; + owner = data.devOwner ?? null; + } else { + console.error("REST fallback for device name returned:", res.status); + } + } catch (err) { + console.error("REST fallback error:", err); + } + } + + applyDevicePageTitle(mac, name, owner); +} + //----------------------------------------------------------------------------------- From 9575692a39d0199c81a35a233f29931f60add1a1 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Sat, 9 May 2026 23:39:47 +0000 Subject: [PATCH 03/12] Fix unsupported object format handling in UpdateFieldAction and DeleteObjectAction --- server/workflows/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/workflows/actions.py b/server/workflows/actions.py index 38edb2956..6169ee49a 100755 --- a/server/workflows/actions.py +++ b/server/workflows/actions.py @@ -69,7 +69,7 @@ def execute(self): mylog("none", f"[WF] Unsupported object format: {obj}") - return obj + return None class DeleteObjectAction(Action): @@ -104,7 +104,7 @@ def execute(self): mylog("none", f"[WF] Unsupported object format: {obj}") - return obj + return None class RunPluginAction(Action): From f428f45ad253a705c3ceebf079d23b3bdea926a2 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Sat, 9 May 2026 23:53:06 +0000 Subject: [PATCH 04/12] Update README to include Domotz as a network monitoring option and refine device management logic in deviceDetails.php --- README.md | 1 + front/deviceDetails.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b646927f..388db71d5 100755 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ Get notified about a new release, what new functionality you can use and about b - [Fing](https://www.fing.com/) - Network scanner app for your Internet security (Commercial, Phone App, Proprietary hardware) - [NetBox](https://netboxlabs.com/) - The gold standard for Network Source of Truth (NSoT) and IPAM. - [Zabbix](https://www.zabbix.com/) or [Nagios](https://www.nagios.org/) - Strong focus on infrastructure monitoring. +- [Domotz](https://www.domotz.com/) - Commercial network monitoring and remote management platform aimed at MSPs, IT teams, and multi-site environments. - [NetAlertX](https://netalertx.com) - The streamlined, discovery-focused choice for real-time asset intelligence and noise-free alerting. ### 💙 Donations diff --git a/front/deviceDetails.php b/front/deviceDetails.php index cd8de5d90..9d3db5491 100755 --- a/front/deviceDetails.php +++ b/front/deviceDetails.php @@ -540,7 +540,7 @@ function applyDevicePageTitle(mac, name, owner) { let owner = getDevDataByMac(mac, "devOwner"); // Stage 2: one re-cache attempt - if (mac !== 'new' && (name === null || owner === null)) { + if (mac !== 'new' && name === null) { console.warn("Device not in cache, attempting re-cache:", mac); showSpinner(); try { @@ -555,7 +555,7 @@ function applyDevicePageTitle(mac, name, owner) { } // Stage 3: REST fallback — same endpoint renderSmallBoxes uses, always DB-direct - if (mac !== 'new' && (name === null || owner === null)) { + if (mac !== 'new' && name === null) { console.warn("Device not found in cache after re-cache, falling back to REST API:", mac); try { const { apiBase, authHeader } = getAuthContext(); From f11a63bb7f48b38080cc6f34bd440e622805b22f Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 13 May 2026 08:43:04 +1000 Subject: [PATCH 05/12] BE: DIGSCAN could not be disbaled due to default RUN set in config.json #1631 --- back/app.conf | 1 + front/plugins/dig_scan/config.json | 2 +- server/initialise.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/back/app.conf b/back/app.conf index 95fcf24f9..4add3c212 100755 --- a/back/app.conf +++ b/back/app.conf @@ -30,6 +30,7 @@ REPORT_DASHBOARD_URL='update_REPORT_DASHBOARD_URL_setting' INTRNT_RUN='schedule' ARPSCAN_RUN='schedule' NSLOOKUP_RUN='before_name_updates' +DIGSCAN_RUN='before_name_updates' AVAHISCAN_RUN='before_name_updates' NBTSCAN_RUN='before_name_updates' diff --git a/front/plugins/dig_scan/config.json b/front/plugins/dig_scan/config.json index fb39b55df..9c176e7fd 100755 --- a/front/plugins/dig_scan/config.json +++ b/front/plugins/dig_scan/config.json @@ -52,7 +52,7 @@ { "elementType": "select", "elementOptions": [], "transformers": [] } ] }, - "default_value": "before_name_updates", + "default_value": "disabled", "options": [ "disabled", "before_name_updates", diff --git a/server/initialise.py b/server/initialise.py index 9509b06bb..bf53c4942 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -488,7 +488,7 @@ def importConfigs(pm, db, all_plugins): # Plugins START # ----------------- - # necessary_plugins = ['UI', 'CUSTPROP', 'CLOUD' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS'] + # necessary_plugins = ['UI', 'CUSTPROP', 'HEARTBEAT' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS'] necessary_plugins = [ "UI", "CUSTPROP", From 13f88583194a173437a89d05f8d222984804d96d Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 13 May 2026 08:43:04 +1000 Subject: [PATCH 06/12] BE: Less verbose SYNC plugin #1164 --- back/app.conf | 1 + front/plugins/dig_scan/config.json | 2 +- server/api_server/sync_endpoint.py | 10 ++++++++-- server/initialise.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/back/app.conf b/back/app.conf index 95fcf24f9..4add3c212 100755 --- a/back/app.conf +++ b/back/app.conf @@ -30,6 +30,7 @@ REPORT_DASHBOARD_URL='update_REPORT_DASHBOARD_URL_setting' INTRNT_RUN='schedule' ARPSCAN_RUN='schedule' NSLOOKUP_RUN='before_name_updates' +DIGSCAN_RUN='before_name_updates' AVAHISCAN_RUN='before_name_updates' NBTSCAN_RUN='before_name_updates' diff --git a/front/plugins/dig_scan/config.json b/front/plugins/dig_scan/config.json index fb39b55df..9c176e7fd 100755 --- a/front/plugins/dig_scan/config.json +++ b/front/plugins/dig_scan/config.json @@ -52,7 +52,7 @@ { "elementType": "select", "elementOptions": [], "transformers": [] } ] }, - "default_value": "before_name_updates", + "default_value": "disabled", "options": [ "disabled", "before_name_updates", diff --git a/server/api_server/sync_endpoint.py b/server/api_server/sync_endpoint.py index 235291613..51d1704bf 100755 --- a/server/api_server/sync_endpoint.py +++ b/server/api_server/sync_endpoint.py @@ -1,13 +1,15 @@ import os import base64 from flask import jsonify, request -from logger import mylog +from logger import mylog, Logger from helper import get_setting_value from utils.datetime_utils import timeNowUTC from messaging.in_app import write_notification INSTALL_PATH = os.getenv("NETALERTX_APP", "/app") +# Make sure log level is initialized correctly +lggr = Logger(get_setting_value('LOG_LEVEL')) def handle_sync_get(): """Handle GET requests for SYNC (NODE → HUB).""" @@ -28,7 +30,11 @@ def handle_sync_get(): response_data = base64.b64encode(raw_data).decode("utf-8") - write_notification("[Plugin: SYNC] Data sent", "info", timeNowUTC()) + message = "[Plugin: SYNC] Data sent" + mylog('verbose', [message]) + if lggr.isAbove('verbose'): + write_notification(message, 'info', timeNowUTC()) + return jsonify({ "node_name": get_setting_value("SYNC_node_name"), "status": 200, diff --git a/server/initialise.py b/server/initialise.py index 9509b06bb..bf53c4942 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -488,7 +488,7 @@ def importConfigs(pm, db, all_plugins): # Plugins START # ----------------- - # necessary_plugins = ['UI', 'CUSTPROP', 'CLOUD' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS'] + # necessary_plugins = ['UI', 'CUSTPROP', 'HEARTBEAT' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS'] necessary_plugins = [ "UI", "CUSTPROP", From 35dc9f9fa0f6ade39f2c12e24c176b6462417387 Mon Sep 17 00:00:00 2001 From: npt-1707 Date: Mon, 18 May 2026 06:15:43 +0800 Subject: [PATCH 07/12] front/lib/moment/moment.js: Avoid loading path-looking locales from fs --- front/lib/moment/moment.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/front/lib/moment/moment.js b/front/lib/moment/moment.js index 1b129716a..5a21cf458 100755 --- a/front/lib/moment/moment.js +++ b/front/lib/moment/moment.js @@ -1842,11 +1842,16 @@ return globalLocale; } + function isLocaleNameSane(name) { + // Prevent names that look like filesystem paths, i.e contain '/' or '\' + return name.match('^[^/\\\\]*$') != null; + } + function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node if (!locales[name] && (typeof module !== 'undefined') && - module && module.exports) { + module && module.exports && isLocaleNameSane(name)) { try { oldLocale = globalLocale._abbr; var aliasedRequire = require; From 0517da24056d081a10cac363ccb4354035ce2513 Mon Sep 17 00:00:00 2001 From: npt-1707 Date: Mon, 18 May 2026 06:17:10 +0800 Subject: [PATCH 08/12] front/lib/moment/moment.js: fix redos using local backtracking regex --- front/lib/moment/moment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/lib/moment/moment.js b/front/lib/moment/moment.js index 1b129716a..b5c592334 100755 --- a/front/lib/moment/moment.js +++ b/front/lib/moment/moment.js @@ -2294,7 +2294,7 @@ function preprocessRFC2822(s) { // Remove comments and folding whitespace and replace multiple-spaces with a single space - return s.replace(/\([^)]*\)|[\n\t]/g, ' ').replace(/(\s\s+)/g, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + return s.replace(/\((?:(?!\().)*\)|[\n\t]/gs, ' ').replace(/(\s\s+)/g, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, ''); } function checkWeekday(weekdayStr, parsedInput, config) { From 80c8a66396149edb452e3943bd47b808e422d4dc Mon Sep 17 00:00:00 2001 From: npt-1707 Date: Mon, 18 May 2026 06:19:59 +0800 Subject: [PATCH 09/12] front/lib/datatables/datatables.js: Fix XSS in Alert, Carousel, Collapse, Dropdown and Modal --- front/lib/datatables/datatables.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/front/lib/datatables/datatables.js b/front/lib/datatables/datatables.js index 2027407d0..168f9fe85 100755 --- a/front/lib/datatables/datatables.js +++ b/front/lib/datatables/datatables.js @@ -10832,7 +10832,8 @@ if (typeof jQuery === 'undefined') { selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = $(selector === '#' ? [] : selector) + selector = selector === '#' ? [] : selector + var $parent = $(document).find(selector) if (e) e.preventDefault() @@ -11228,9 +11229,15 @@ if (typeof jQuery === 'undefined') { // ================= var clickHandler = function (e) { - var href var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + var href = $this.attr('href') + if (href) { + href = href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + } + + var target = $this.attr('data-target') || href + var $target = $(document).find(target) + if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') @@ -11420,7 +11427,7 @@ if (typeof jQuery === 'undefined') { var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - return $(target) + return $(document).find(target) } @@ -11502,7 +11509,7 @@ if (typeof jQuery === 'undefined') { selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = selector && $(selector) + var $parent = selector && $(document).find(selector) return $parent && $parent.length ? $parent : $this.parent() } @@ -11961,7 +11968,10 @@ if (typeof jQuery === 'undefined') { $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var target = $this.attr('data-target') || + (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + + var $target = $(document).find(target) var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() From bc87e3984335359d4c37c784104f85699d23e249 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Mon, 18 May 2026 02:45:23 +0000 Subject: [PATCH 10/12] Enhance documentation and improve formatting across multiple files --- docs/ADVISORY_EYES_ON_GLASS.md | 3 +++ docs/ADVISORY_MULTI_NETWORK.md | 18 ++++++++++++------ docs/PLUGINS_DEV_UI_COMPONENTS.md | 2 ++ docs/WORKFLOW_EXAMPLES.md | 28 ++++++++++++++-------------- front/css/app.css | 1 + 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/docs/ADVISORY_EYES_ON_GLASS.md b/docs/ADVISORY_EYES_ON_GLASS.md index ace8f0dcc..41803b762 100644 --- a/docs/ADVISORY_EYES_ON_GLASS.md +++ b/docs/ADVISORY_EYES_ON_GLASS.md @@ -4,6 +4,9 @@ For Managed Service Providers (MSPs) and Network Operations Centers (NOC), "Eyes ![filters](./img/ADVISORIES/filters.png) +> [!TIP] +> If you are using Grafana check the `/metrics` endpoint that exposes **Prometheus-compatible metrics** for NetAlertX, including aggregate device counts and per-device status. See the [Metrics API endpoint](./API_METRICS.md) documentation for details. + --- ### 1. Configure Auto-Refresh for Live Monitoring diff --git a/docs/ADVISORY_MULTI_NETWORK.md b/docs/ADVISORY_MULTI_NETWORK.md index 7a0dd0948..c26a4f6e7 100644 --- a/docs/ADVISORY_MULTI_NETWORK.md +++ b/docs/ADVISORY_MULTI_NETWORK.md @@ -12,20 +12,25 @@ Effective multi-network monitoring starts with understanding how NetAlertX "sees * **Manual Entry:** For static assets where only ICMP (ping) status is needed. > [!TIP] -> Explore the [remote networks](./REMOTE_NETWORKS.md) documentation for more details on how to set up the approaches menationed above. +> Explore the [remote networks](./REMOTE_NETWORKS.md) documentation for more details on how to set up the approaches mentioned above. --- ### 2. Automating IT Asset Inventory with Workflows -[Workflows](./WORKFLOWS.md) are the "engine" of NetAlertX, reducing manual overhead as your device list grows. +[Workflows](./WORKFLOWS.md) are the "engine" of NetAlertX, reducing manual overhead as your device list grows. See some examples below.: + +#### A. Logical Ownership & VLAN Tagging + +Create a workflow triggered on **Device Creation** to: -* **A. Logical Ownership & VLAN Tagging:** Create a workflow triggered on **Device Creation** to: 1. Inspect the IP/Subnet. 2. Set `devVlan` or `devOwner` custom fields automatically. +#### B. Auto-Grouping: + +Use conditional logic to categorize devices. -* **B. Auto-Grouping:** Use conditional logic to categorize devices. * *Example:* If `devLastIP == 10.10.20.*`, then `Set devLocation = "BranchOffice"`. ```json @@ -56,9 +61,9 @@ Effective multi-network monitoring starts with understanding how NetAlertX "sees ] } ``` +#### C. Sync Node Tracking - -* **C. Sync Node Tracking:** When using multiple instances, ensure all synchub nodes have a descriptive `SYNC_node_name` name to distinguish between sites. +When using multiple instances, ensure all synchub nodes have a descriptive `SYNC_node_name` name to distinguish between sites. > [!TIP] > Always test new workflows in a "Staging" instance. A misconfigured workflow can trigger thousands of unintended updates across your database. @@ -107,6 +112,7 @@ As your environment grows, tuning the underlying engine is vital to maintain a s * **Plugin Scheduling:** Avoid "Scan Storms" by staggering plugin execution. Running intensive tasks like `NMAP` or `MASS_DNS` simultaneously can spike CPU and cause database locks. * **Database Health:** Large-scale monitoring generates massive event logs. Use the **[DBCLNP (Database Cleanup)](https://www.google.com/search?q=https://docs.netalertx.com/PLUGINS/%23dbclnp)** plugin to prune old records and keep the SQLite database performant. * **Resource Management:** For high-device counts, consider increasing the memory limit for the container and utilizing `tmpfs` for temporary files to reduce SD card/disk I/O bottlenecks. +* Enable the `DEEP_SLEEP` setting. > [!IMPORTANT] > For a deep dive into hardware requirements, database vacuuming, and specific environment variables for high-load instances, refer to the full **[Performance Optimization Guide](https://docs.netalertx.com/PERFORMANCE/)**. diff --git a/docs/PLUGINS_DEV_UI_COMPONENTS.md b/docs/PLUGINS_DEV_UI_COMPONENTS.md index 663f52e0f..ef2d75fdf 100644 --- a/docs/PLUGINS_DEV_UI_COMPONENTS.md +++ b/docs/PLUGINS_DEV_UI_COMPONENTS.md @@ -7,6 +7,7 @@ Configure how your plugin's data is displayed in the NetAlertX web interface. Plugin results are displayed in the UI via the **Plugins page** and **Device details tabs**. You control the appearance and functionality of these displays by defining `database_column_definitions` in your plugin's `config.json`. Each column definition specifies: + - Which data field to display - How to render it (label, link, color-coded badge, etc.) - What CSS classes to apply @@ -275,6 +276,7 @@ Replaces specific values with display strings or HTML. ``` **Output Examples:** + - `"online"` → 🟢 Online - `"offline"` → 🔴 Offline - `"idle"` → 🟡 Idle diff --git a/docs/WORKFLOW_EXAMPLES.md b/docs/WORKFLOW_EXAMPLES.md index a6c1b3f06..5f5da444b 100755 --- a/docs/WORKFLOW_EXAMPLES.md +++ b/docs/WORKFLOW_EXAMPLES.md @@ -45,22 +45,22 @@ Sometimes devices are manually archived (e.g., no longer expected on the network ], "enabled": "Yes" } -``` +```` ### 🔍 Explanation - - Trigger: Listens for updates to device records. - - Conditions: - - `devIsArchived` is `1` (archived). - - `devPresentLastScan` is `1` (device was detected in the latest scan). - - Action: Updates the device to set `devIsArchived` to `0` (unarchived). +* **Trigger**: Listens for updates to device records. +* **Conditions**: -### ✅ Result + * `devIsArchived` is `1` (archived). + * `devPresentLastScan` is `1` (device was detected in the latest scan). +* **Action**: -Whenever a previously archived device shows up during a network scan, it will be automatically unarchived — allowing it to reappear in your device lists and dashboards. + * Updates the device to set `devIsArchived` to `0` (unarchived). +### ✅ Result -Here is your updated version of **Example 2** and **Example 3**, fully aligned with the format and structure of **Example 1** for consistency and professionalism: +Whenever a previously archived device shows up during a network scan, it will be automatically unarchived — allowing it to reappear in your device lists and dashboards. --- @@ -107,7 +107,7 @@ When new devices join your network, assigning them to the correct network node i ### 🔍 Explanation * **Trigger**: Activates when a new device is added. -* **Condition**: +* **Conditions**: * `devLastIP` contains `192.168.1.` (matches subnet). * **Action**: @@ -173,12 +173,12 @@ You may want to automatically clear out newly detected Google devices (such as C * **Trigger**: Runs on device updates. * **Conditions**: - * Vendor contains `Google`. - * Device is marked as new (`devIsNew` is `1`). + * `devVendor` contains `Google`. + * `devIsNew` is `1` (device marked as new). * **Actions**: - 1. Set `devIsNew` to `0` (mark as not new). - 2. Delete the device. + 1. Sets `devIsNew` to `0` (mark as not new). + 2. Deletes the device. ### ✅ Result diff --git a/front/css/app.css b/front/css/app.css index df77fc02e..24bbdf6b6 100755 --- a/front/css/app.css +++ b/front/css/app.css @@ -1417,6 +1417,7 @@ textarea[readonly], grid-template-columns: repeat(auto-fit, minmax(125px, 1fr)); gap: 0.75em; padding: 0.5em 0; + padding-top: 0; } #columnFilters::before, From 5f62d25e23eb1fc53b4a37b8e376e28cfe2aeb22 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Thu, 21 May 2026 00:09:07 +0000 Subject: [PATCH 11/12] Implement XSS prevention by encoding special characters in device names across multiple files --- front/deviceDetails.php | 4 +- front/deviceDetailsEdit.php | 6 + front/devices.php | 61 ++++--- front/events.php | 8 +- front/js/common.js | 72 ++++++-- front/js/network-api.js | 2 +- front/js/network-tabs.js | 8 +- front/js/network-tree.js | 4 +- front/js/settings_utils.js | 4 +- front/js/ui_components.js | 4 +- front/multiEditCore.php | 2 +- front/presence.php | 2 +- test/ui/test_ui_xss_devname.py | 322 +++++++++++++++++++++++++++++++++ 13 files changed, 437 insertions(+), 62 deletions(-) create mode 100644 test/ui/test_ui_xss_devname.py diff --git a/front/deviceDetails.php b/front/deviceDetails.php index 9d3db5491..050672641 100755 --- a/front/deviceDetails.php +++ b/front/deviceDetails.php @@ -517,11 +517,11 @@ function applyDevicePageTitle(mac, name, owner) { ); $('#devicePageInfoPlc').show(); } else if (!owner || (name && name.toString().includes(owner))) { - pageTitleText = name ?? getString("DevDetail_EveandAl_NewDevice"); + pageTitleText = encodeSpecialChars(name ?? getString("DevDetail_EveandAl_NewDevice")); $('#pageTitle').html(pageTitleText); $('#devicePageInfoPlc').hide(); } else { - pageTitleText = `${name ?? getString("DevDetail_EveandAl_NewDevice")} (${owner})`; + pageTitleText = `${encodeSpecialChars(name ?? getString("DevDetail_EveandAl_NewDevice"))} (${encodeSpecialChars(owner)})`; $('#pageTitle').html(pageTitleText); $('#devicePageInfoPlc').hide(); } diff --git a/front/deviceDetailsEdit.php b/front/deviceDetailsEdit.php index 542f2a595..99f22bf69 100755 --- a/front/deviceDetailsEdit.php +++ b/front/deviceDetailsEdit.php @@ -236,6 +236,7 @@ function getDeviceData() { // console.log(setting.setKey); // console.log(fieldData); + // Additional form elements like the random MAC address button for devMac let inlineControl = ""; // handle random mac @@ -329,6 +330,11 @@ function getDeviceData() { fieldOptionsOverride = fieldDataNew; } + // XSS prevention - encode special characters for string fields, but not for arrays (like children dynamic) + // Don't move above the handle devChildrenDynamic block because it relies on the original fieldData to generate options + fieldData = encodeSpecialChars(fieldData); + console.log(fieldData); + // Generate the input field HTML const inputFormHtml = `