mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-09-17 08:44:42 -04:00
Merge pull request #1753 from netalertx/next_release
FE: Implement pause/resume functionality for automatic scans with API…
This commit is contained in:
45 files changed
+926
-129
No files matched your search
@@ -98,6 +98,15 @@ from db_test_helpers import make_db, DummyDB, insert_device, minutes_ago
|
||||
|
||||
If a helper you need doesn't exist yet, add it to `db_test_helpers.py` — not locally in the test file.
|
||||
|
||||
## Stubbing Modules in Standalone-Capable Tests
|
||||
|
||||
If a test stubs NetAlertX modules into `sys.modules` so a script can be imported
|
||||
outside the container (see `test/plugins/test_ntfy_custom_headers.py`), pop each
|
||||
stubbed name back out of `sys.modules` right after the one-time import that needed
|
||||
it. Otherwise the fake module leaks into every other test file collected in the
|
||||
same pytest session and shadows the real module (see `testing-workflow` skill for
|
||||
the full pattern and reproduction steps).
|
||||
|
||||
## MAC Literals in Tests — ALWAYS Lowercase
|
||||
|
||||
**MANDATORY:** Every MAC address literal used in test fixtures, parametrize decorators, assertions, or comments must be lowercase hex:
|
||||
|
||||
@@ -59,3 +59,49 @@ docker buildx build -t netalertx-test .
|
||||
```
|
||||
|
||||
This takes ~30 seconds unless venv stage changes (~90s).
|
||||
|
||||
## Pitfall: `sys.modules` Stubbing Leaks Across Test Files
|
||||
|
||||
Some plugin tests (e.g. `test/plugins/test_ntfy_custom_headers.py`) stub NetAlertX
|
||||
modules (`conf`, `helper`, `models.notification_instance`, etc.) via
|
||||
`sys.modules[name] = fake_module` so the plugin script can be imported standalone,
|
||||
outside the container. Because `sys.modules` is a single process-wide cache shared
|
||||
by the whole pytest session, a fake module inserted by one test file silently
|
||||
shadows the real module for every other test file collected afterwards — pytest
|
||||
imports all test files during collection, before any test runs, so this can happen
|
||||
regardless of alphabetical/directory order.
|
||||
|
||||
Symptom: `AttributeError: <module 'models.notification_instance'> does not have
|
||||
the attribute 'get_setting_value'` (or similar) in an unrelated test file, where
|
||||
the module repr has no `from '<path>'` suffix — a giveaway that a stub, not the
|
||||
real module, was resolved.
|
||||
|
||||
Fix pattern: track which module names your stub actually inserted, and pop them
|
||||
back out of `sys.modules` immediately after the one-time import that needed them
|
||||
(the already-imported script keeps its bound names regardless):
|
||||
|
||||
```python
|
||||
_stubbed_module_names = []
|
||||
|
||||
def _stub(name, **attrs):
|
||||
if name not in sys.modules:
|
||||
mod = types.ModuleType(name)
|
||||
for k, v in attrs.items():
|
||||
setattr(mod, k, v)
|
||||
sys.modules[name] = mod
|
||||
_stubbed_module_names.append(name)
|
||||
|
||||
# ... _stub(...) calls, then the one-time import ...
|
||||
import ntfy
|
||||
|
||||
for _name in _stubbed_module_names:
|
||||
sys.modules.pop(_name, None)
|
||||
```
|
||||
|
||||
Reproduce cross-file pollution locally by running the suspect file together with
|
||||
the affected one in a single pytest invocation (order matters less than you'd
|
||||
think — collection happens for all files first):
|
||||
|
||||
```bash
|
||||
pytest test/plugins/test_ntfy_custom_headers.py test/backend/test_notification_templates.py -v
|
||||
```
|
||||
@@ -27,7 +27,6 @@ NetAlertX gives you a real-time source of truth for connected devices, helps ide
|
||||
|
||||
Use NetAlertX to spot shadow IT, unauthorized hardware, IPAM drift, and other changes that matter to service teams. With multi-site sync, reporting, workflows, and webhooks, it helps MSPs stay ahead of problems without the overhead of a full NMS or SIEM.
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
|
||||
@@ -110,7 +110,7 @@ function getDeviceData() {
|
||||
// columns to hide
|
||||
hiddenFields = ["NEWDEV_devScan", "NEWDEV_devPresentLastScan"]
|
||||
// columns to disable/readonly - conditional depending if a new dummy device is created
|
||||
disabledFields = mac == "new" ? ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection"] : ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devMac", "NEWDEV_devLastIP", "NEWDEV_devPrimaryIPv6", "NEWDEV_devPrimaryIPv4", "NEWDEV_devSyncHubNode", "NEWDEV_devFQDN"];
|
||||
disabledFields = mac == "new" ? ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devFQDN", "NEWDEV_devPrimaryIPv4", "NEWDEV_devPrimaryIPv6", "NEWDEV_devSyncHubNode"] : ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devMac", "NEWDEV_devLastIP", "NEWDEV_devPrimaryIPv6", "NEWDEV_devPrimaryIPv4", "NEWDEV_devSyncHubNode", "NEWDEV_devFQDN"];
|
||||
|
||||
// Fields that are tracked by authoritative handler and can be locked/unlocked
|
||||
const trackedFields = {
|
||||
|
||||
@@ -13,7 +13,7 @@ function renderNetworkTabs(nodes) {
|
||||
(node.devAlertDown == 1 ? "text-red" : "text-gray50"));
|
||||
|
||||
const portLabel = node.node_ports_count ? ` (${node.node_ports_count})` : '';
|
||||
const icon = atob(node.devIcon);
|
||||
const icon = safeAtob(node.devIcon);
|
||||
const id = node.devMac.replace(/:/g, '_');
|
||||
|
||||
html += `
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//--------------------------------------------------------------
|
||||
// Pause / Resume automatic scans button
|
||||
// Default pause duration (minutes) used for the single-click header button
|
||||
function renderPauseResumeButton(pauseUntil) {
|
||||
const icon = document.getElementById('pause-resume-icon');
|
||||
const link = document.getElementById('pause-resume-button');
|
||||
if (!icon || !link) return;
|
||||
|
||||
const isPaused = !!pauseUntil;
|
||||
icon.className = isPaused ? 'fa-solid fa-play' : 'fa-solid fa-pause';
|
||||
link.title = isPaused
|
||||
? getString('Header_ResumeScans_Tooltip')
|
||||
: getString('Header_PauseScans_Tooltip');
|
||||
}
|
||||
|
||||
// Updated whenever the SSE state manager receives a state_update event (see sse_manager.js)
|
||||
document.addEventListener('nax:pauseStateUpdate', (e) => {
|
||||
renderPauseResumeButton(e.detail.pauseUntil);
|
||||
});
|
||||
|
||||
function togglePauseScans() {
|
||||
const PAUSE_SCANS_DEFAULT_MINUTES = getSetting("UI_SCAN_PAUSE");
|
||||
const icon = document.getElementById('pause-resume-icon');
|
||||
const isPaused = icon && icon.classList.contains('fa-play');
|
||||
const apiBase = getApiBase();
|
||||
const apiToken = getSetting("API_TOKEN");
|
||||
const endpoint = isPaused ? '/scan/resume' : '/scan/pause';
|
||||
const success_msg = isPaused ? getString("Scans_Resumed") : getString("Scans_Paused");
|
||||
const payload = isPaused ? {} : { minutes: PAUSE_SCANS_DEFAULT_MINUTES };
|
||||
|
||||
$.ajax({
|
||||
url: `${apiBase}${endpoint}`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||
data: JSON.stringify(payload),
|
||||
error: function(xhr, status, error) {
|
||||
console.error("[Header] Error toggling scan pause:", status, error);
|
||||
showMessage(error, 5000, "modal_red");
|
||||
},
|
||||
success:function() {
|
||||
showMessage(success_msg);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,6 +186,13 @@ class NetAlertXStateManager {
|
||||
}));
|
||||
}
|
||||
|
||||
// 6. Dispatch pause state update for the header Pause/Resume button
|
||||
if (appState["pause_until"] !== undefined) {
|
||||
document.dispatchEvent(new CustomEvent('nax:pauseStateUpdate', {
|
||||
detail: { pauseUntil: appState["pause_until"] }
|
||||
}));
|
||||
}
|
||||
|
||||
// console.log("[NetAlertX State] UI updated via jQuery");
|
||||
} catch (e) {
|
||||
console.error("[NetAlertX State] Failed to update state display:", e);
|
||||
|
||||
@@ -971,6 +971,9 @@ function renderDeviceLink(data, container, useName = false) {
|
||||
// Build and return badge parts
|
||||
const badge = badgeFromDevice(device);
|
||||
|
||||
// Decode once (with a safe fallback) and reuse for both the chip and hover preview
|
||||
const decodedIcon = safeAtob(device.devIcon);
|
||||
|
||||
// badge class and hover-info class to container
|
||||
$(container)
|
||||
.addClass(`${badge.cssClass} hover-node-info`)
|
||||
@@ -989,14 +992,14 @@ function renderDeviceLink(data, container, useName = false) {
|
||||
'data-alertdown': device.devAlertDown,
|
||||
'data-sleeping': device.devIsSleeping || 0,
|
||||
'data-archived': device.devIsArchived || 0,
|
||||
'data-isnew': device.devIsNew || 0,
|
||||
'data-icon': device.devIcon
|
||||
'data-isnew': device.devIsNew || 0,
|
||||
'data-icon': decodedIcon
|
||||
});
|
||||
|
||||
return `
|
||||
<a href="${badge.url}" target="_blank">
|
||||
<span class="custom-chip">
|
||||
<span class="iconPreview">${atob(device.devIcon)}</span>
|
||||
<span class="iconPreview">${decodedIcon}</span>
|
||||
${useName ? encodeSpecialChars(device.devName) : data.text}
|
||||
<span>
|
||||
(${badge.iconHtml})
|
||||
@@ -1006,6 +1009,17 @@ function renderDeviceLink(data, container, useName = false) {
|
||||
`;
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
// Base64-decode a devIcon value, tolerating missing/empty/malformed input
|
||||
function safeAtob(value) {
|
||||
if (!value) return '';
|
||||
try {
|
||||
return atob(value);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
// Display device info on hover (attach only once)
|
||||
function initHoverNodeInfo() {
|
||||
@@ -1063,7 +1077,7 @@ function initHoverNodeInfo() {
|
||||
|
||||
const html = `
|
||||
<div>
|
||||
<b> <div class="iconPreview">${atob(icon)}</div> </b><b class="devName"> ${encodeSpecialChars(name)}</b><br>
|
||||
<b> <div class="iconPreview">${icon || ''}</div> </b><b class="devName"> ${encodeSpecialChars(name)}</b><br>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="line">
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<script src="js/db_methods.js?v=<?php include 'php/templates/version.php'; ?>"></script>
|
||||
<script src="js/settings_utils.js?v=<?php include 'php/templates/version.php'; ?>"></script>
|
||||
<script src="js/device.js?v=<?php include 'php/templates/version.php'; ?>"></script>
|
||||
<script src="js/scan_control.js?v=<?php include 'php/templates/version.php'; ?>"></script>
|
||||
|
||||
<!-- iCheck -->
|
||||
|
||||
@@ -208,11 +209,17 @@
|
||||
<li>
|
||||
<a id="fullscreen-button" href='#' role="button" span class='fa fa-arrows-alt' onclick='toggleFullscreen()'></a>
|
||||
</li>
|
||||
<!-- Pause / Resume automatic scans -->
|
||||
<li>
|
||||
<a id="pause-resume-button" href="#" role="button" title="<?= lang('Header_PauseScans_Tooltip') ?>" onclick="togglePauseScans(); return false;">
|
||||
<i id="pause-resume-icon" class="fa-solid fa-pause"></i>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Notifications -->
|
||||
<li>
|
||||
<a id="notifications-button" href='userNotifications.php' role="button" span class='fa-solid fa-bell'></a>
|
||||
<span id="unread-notifications-bell-count" title="" class="badge bg-red unread-notifications-bell" >0</span>
|
||||
</li>
|
||||
</li>
|
||||
<!-- Server Status -->
|
||||
<li>
|
||||
<a onclick="setCache('activeMaintenanceTab', 'tab_Logging_id')" href="maintenance.php#tab_Logging">
|
||||
@@ -482,16 +489,14 @@
|
||||
|
||||
function toggleFullscreen() {
|
||||
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
else {
|
||||
document.documentElement.requestFullscreen();
|
||||
}
|
||||
else {
|
||||
document.documentElement.requestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------
|
||||
|
||||
// Update server time in the header
|
||||
update_servertime()
|
||||
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "مدة الاحتفاظ بالأجهزة الجديدة",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "عدد الساعات للاحتفاظ بالأجهزة غير المتصلة",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "مدة الاحتفاظ بالأجهزة غير المتصلة",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "المكونات الإضافية المحملة",
|
||||
"LOADED_PLUGINS_name": "المكونات الإضافية المحملة",
|
||||
"LOG_LEVEL_description": "مستوى السجلات",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "الشبكات الفرعية للفحص",
|
||||
"SCAN_SUBNETS_name": "شبكات الفحص",
|
||||
"SYSTEM_TITLE": "عنوان النظام",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "تجاوز الإعدادات",
|
||||
"Setting_Override_Description": "وصف تجاوز الإعدادات",
|
||||
"Settings_Metadata_Toggle": "إظهار/إخفاء البيانات الوصفية للإعداد المحدد.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Eliminar nous dispositius després de",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Això és un paràmetre de manteniment <b>ELIMINANT dispositius</b>. Si s'activa (<code>0</code> està desactivat), els dispositius que estan <b>Offline</b> i el seu temps <b>Last Offline</b> es més vell que les hores especificades en aquest paràmetre, s'esborraran. Faci servir aquest paràmetre si vol auto-eliminar <b>Dispositius Offline</b> després de <code>X</code> hores sense connexió.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Eliminar dispositius fora de línia després",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Quins Plugins carregar. Afegir plugins podria alentir l'aplicació. Llegir més sobre quins connectors necessiten estar habilitats, els tipus, o les opcions d'escaneig dins del <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">documents de connectors</a>. Els connectors descarregats perdran els vostres paràmetres. Només <code>desactivats</code> es poden eliminar els connectors.",
|
||||
"LOADED_PLUGINS_name": "Connectors carregats",
|
||||
"LOG_LEVEL_description": "Aquest paràmetre permetrà un registre més detallat. Útil per a la depuració d'esdeveniments d'escriptura a la base de dades.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "La majoria dels escàners en xarxa (ARP-SCAN, NMAP, NSLOOKUP, DIG) es basen en l'exploració d'interfícies de xarxa específiques i subxarxes. Comproveu la <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentació de subxarxes</a> per ajudar en aquesta configuració, especialment VLANs, i quines VLANs són compatibles, o com esbrinar la màscara de xarxa i la seva interfície. <br/> <br/> Una alternativa als escàners en xarxa és activar alguns altres escàners / importadors de dispositius que no requereixin NetAlert<sup>X</sup> per tenir accés a la xarxa (UNIFI, dhcp. leases, PiHole, etc.). <br/> <br/> Nota: El temps d'exploració en si mateix depèn del nombre d'adreces IP per verificar, així que s'ha establir amb cura amb la màscara i la interfície de xarxa adequats.",
|
||||
"SCAN_SUBNETS_name": "Xarxes per escanejar",
|
||||
"SYSTEM_TITLE": "Informació de sistema",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Valor de sobreescriptura",
|
||||
"Setting_Override_Description": "Activant aquesta opció anul·larà un valor predeterminat de l'aplicació amb el valor especificat.",
|
||||
"Settings_Metadata_Toggle": "Mostrar/amagar metadades per a la configuració donada.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Odstranit nová zařízení po",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Toto je nastavení údržby <b>ODSTRANĚNÍ zařízení</b>. Pokud je povoleno (<code>0</code> zakázáno), zařízení <b>Offline</b> a data jejich <b>Posledního připojení</b> starší, než uvedené hodiny v tomto nastavení, budou odstraněna. Toto nastavení použijte, pokud chcete automaticky mazat <b>Offline zařízení</b> po uplynutí <code>X</code> hodin offline.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Odstranit offline zařízení po",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Které zásuvné moduly načíst. Přidávání modulů může aplikaci zpomalit. Přečtěte si více o tom, které, které je třeba, aby byly povolené, o jejich typech nebo o předvolbách skenování v <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">dokumentaci k zásuvným modulům</a>. Odpojené moduly ztratí vaše nastavení. Odpojit je možné pouze <code>deaktivované</code> moduly.",
|
||||
"LOADED_PLUGINS_name": "Načtené moduly",
|
||||
"LOG_LEVEL_description": "Toto nastavení zapne podrobnější zaznamenávání událostí. To je užitečné pro ladění událostí zapisujících do databáze.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "Většina skenerů sítí (ARP-SCAN, NMAP, NSLOOKUP, DIG) spoléhá na skenování konkrétních síťových rozhraní a podsítí. Podívejte se do <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">dokumentace k podsítím</a> ohledně pokynů k tomuto uspořádání, zejména VLAN sítím, ohledně toho, které VLAN sítě jsou podporovány nebo jak nastavit masku sítě na svém rozhraní. <br/> <br/> Alternativou ke skenerům na sítích je zapnout nějaké jiné skenery/importéry rozhraní, které nezávisí na tom, aby NetAlert<sup>X</sup> mělo přístup k síti (UNIFI, dhcp.leases, PiHole, atd.). <br/> <br/> Pozn.: Doba skenování jako taková závisí na počtu IP adres, které zkontrolovat, takže toto nastavte pečlivě s příslušnou maskou sítě a rozhraním.",
|
||||
"SCAN_SUBNETS_name": "Sítě ke skenování",
|
||||
"SYSTEM_TITLE": "Informace o systému",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Přebít hodnotu",
|
||||
"Setting_Override_Description": "Zapnutí této předvolby přebije výchozí hodnotu z aplikace hodnotou, uvedenou výše.",
|
||||
"Settings_Metadata_Toggle": "Zobrazit/skrýt metadata pro dané nastavení.",
|
||||
|
||||
@@ -391,6 +391,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Neue Geräte löschen nach",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Offline-Geräte löschen nach",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "Geladene Plugins",
|
||||
"LOG_LEVEL_description": "Diese Einstellung aktiviert die erweiterte Protokollierung. Nützlich fürs Debuggen von in die Datenbank geschriebenen Events.",
|
||||
@@ -706,6 +708,8 @@
|
||||
"SMTP_USER_description": "The user name used to login into the SMTP server (sometimes a full email address).",
|
||||
"SMTP_USER_name": "SMTP user",
|
||||
"SYSTEM_TITLE": "Systeminformationen",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Wert überschreiben",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "Metadaten für die angegebene Einstellung anzeigen/ausblenden.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Delete new devices after",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "This is a maintenance setting <b>DELETING devices</b>. If enabled (<code>0</code> is disabled), devices that are <b>Offline</b> and their <b>Last Connection</b> date time is older than the specified hours in this setting, will be deleted. Use this setting if you want to auto-delete <b>Offline devices</b> after <code>X</code> hours being offline.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Delete offline devices after",
|
||||
"Header_PauseScans_Tooltip": "Pause automatic scans",
|
||||
"Header_ResumeScans_Tooltip": "Resume automatic scans",
|
||||
"LOADED_PLUGINS_description": "Which Plugins to load. Adding plugins might slow the application. Read more about which plugins need to be enabled, types, or scanning options in the <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins docs</a>. Unloaded plugins will lose your settings. Only <code>disabled</code> plugins can be unloaded.",
|
||||
"LOADED_PLUGINS_name": "Loaded plugins",
|
||||
"LOG_LEVEL_description": "This setting will enable more verbose logging. Useful for debugging events writing into the database.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "Most on-network scanners (ARP-SCAN, NMAP, NSLOOKUP, DIG) rely on scanning specific network interfaces and subnets. Check the <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">subnets documentation</a> for help on this setting, especially VLANs, what VLANs are supported, or how to figure out the network mask and your interface. <br/> <br/> An alternative to on-network scanners is to enable some other device scanners/importers that don't rely on NetAlert<sup>X</sup> having access to the network (UNIFI, dhcp.leases, PiHole, etc.). <br/> <br/> Note: The scan time itself depends on the number of IP addresses to check, so set this up carefully with the appropriate network mask and interface.",
|
||||
"SCAN_SUBNETS_name": "Networks to scan",
|
||||
"SYSTEM_TITLE": "System Information",
|
||||
"Scans_Paused": "Scans paused",
|
||||
"Scans_Resumed": "Scans resumed",
|
||||
"Setting_Override": "Override value",
|
||||
"Setting_Override_Description": "Enabling this option will override an App supplied default value with the value specified above.",
|
||||
"Settings_Metadata_Toggle": "Show/hide metadata for the given setting.",
|
||||
|
||||
@@ -389,6 +389,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Eliminar nuevos dispositivos después",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Esta es una configuración de mantenimiento <b>BORRAR dispositivos</b>. Si está activado (<code>0</code> está desactivado), los dispositivos que están <b>Sin Conexión</b> y su fecha de <b>Última Conexión</b> es anterior a las horas especificadas en este ajuste se eliminarán. Use este ajuste si desea eliminar automáticamente <b>los dispositivos sin conexión</b> después de que el <code>X</code> horas esté sin conexión.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Borrar dispositivos sin conexión después de",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "¿Qué plugins cargar?. Agregar plugins puede ralentizar la aplicación. Obtén más información sobre los complementos que deben habilitarse, los tipos o las opciones de escaneo en los documentos de <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. Los plugins descargados perderán tu configuración. Solo se pueden descargar los complementos <code>deshabilitados</code>.",
|
||||
"LOADED_PLUGINS_name": "Plugins cargados",
|
||||
"LOG_LEVEL_description": "Esto hará que el registro tenga más información. Util para depurar que eventos se van guardando en la base de datos.",
|
||||
@@ -704,6 +706,8 @@
|
||||
"SMTP_USER_description": "El nombre de usuario utilizado para iniciar sesión en el servidor SMTP (a veces, una dirección de correo electrónico completa).",
|
||||
"SMTP_USER_name": "Nombre de usuario SMTP",
|
||||
"SYSTEM_TITLE": "Información del sistema",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Sobreescribir el valor",
|
||||
"Setting_Override_Description": "Habilitar esta opción anulará un valor predeterminado proporcionado por la aplicación con el valor especificado anteriormente.",
|
||||
"Settings_Metadata_Toggle": "Mostrar/ocultar los metadatos de la configuración.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "",
|
||||
"LOG_LEVEL_description": "",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "",
|
||||
"LOG_LEVEL_description": "",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Supprimer les nouveaux appareils après",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Il s'agit d'un paramètre de maintenance <b>SUPPRIMER des appareils</b>. Si cette option est activée (<code>0</code> est désactivé), les appareils qui sont <b>Hors ligne</b> et dont la <b>dernière connexion</b> est plus ancienne que les heures spécifiées dans ce paramètre. Utilisez ce paramètre si vous souhaitez supprimer automatiquement <b>Appareils hors ligne</b> après <code>X</code> heures de déconnexion.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Supprimez les appareils hors ligne après",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Affiche les plugins chargés. Ajouter des plugins peut ralentir l'application. Obtenez plus d'informations dur quels plugins dont à activer, ou les options de scan dans la <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">documentation des plugins</a>. Décharger des plugins leur fait perdre leurs paramètres. Seuls les plugins <code>désactivés</code> peuvent être déchargés.",
|
||||
"LOADED_PLUGINS_name": "Plugins chargés",
|
||||
"LOG_LEVEL_description": "Ce paramètre active une journalisation dans les logs plus verbeuse. Cela est utile pour identifier les événements écrivant dans la base de données.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "La plupart des scanners sur le réseau (scan ARP, NMAP, Nslookup, DIG) se base sur le scan d'une partie spécifique des interfaces réseau ou de sous-réseau. Consulter la <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentation des sous-réseaux</a> pour plus d'aide sur ce paramètre, notamment pour des VLAN, lesquels sont supportés ou sur comment identifier le masque réseau et votre interface réseau. <br/> <br/> Une alternative à ces scanner sur le réseau et d'activer d'autres scanners d'appareils ou des importe, qui ne dépendent pas du fait de laisser NetAlert<sup>X</sup> accéder au réseau (Unifié, baux DHCP, Pi-hole, etc.).<br/><br/> Remarque : la durée du scan en lui-même dépend du nombre d'adresses IP à scanner, renseignez donc soigneusement avec le bon masque réseau et la bonne interface réseau.",
|
||||
"SCAN_SUBNETS_name": "Réseaux à scanner",
|
||||
"SYSTEM_TITLE": "Informations système",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Remplacer la valeur",
|
||||
"Setting_Override_Description": "Activer cette option va remplacer la valeur fournie par défaut par une application par la valeur renseignée au-dessus.",
|
||||
"Settings_Metadata_Toggle": "Afficher/masquer les méta données pour le paramètre sélectionné.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "",
|
||||
"LOG_LEVEL_description": "",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "",
|
||||
"LOG_LEVEL_description": "",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Elimina nuovi dispositivi dopo",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Questa è un'impostazione di manutenzione che <b>ELIMINA dispositivi</b>. Se abilitata (<code>0</code> è disabilitata), i dispositivi <b>Offline</b> la cui data e ora di <b>Ultima connessione</b> sono antecedenti alle ore specificate in questa impostazione, verranno eliminati. Utilizza questa impostazione se vuoi eliminare automaticamente i <b>Dispositivi offline</b> dopo <code>X</code> ore trascorse offline.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Elimina dispositivi offline dopo",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Quali Plugin caricare. L'aggiunta di plugin potrebbe rallentare l'applicazione. Leggi di più su quali plugin necessitano di essere abilitati, tipi e opzioni di scansione nella <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">documentazione plugin</a>. I plugin disinstallati perdono la loro configurazione. Solo i plugin <code>disabilitati</code> possono essere disinstallati.",
|
||||
"LOADED_PLUGINS_name": "Plugin caricati",
|
||||
"LOG_LEVEL_description": "Questa impostazione abilita un log più dettagliato. Utile per il debug degli eventi salvati nel database.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "La maggior parte degli scanner di rete (ARP-SCAN, NMAP, NSLOOKUP, DIG) si basano sulla scansione di interfacce di rete e sottoreti specifiche. Consulta la <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentazione sulle sottoreti</a> per assistenza su questa impostazione, in particolare VLAN, quali VLAN sono supportate o come individuare la maschera di rete e l'interfaccia. <br/> <br/> Un'alternativa agli scanner in rete è abilitare altri scanner/importatori di dispositivi che non si affidano a NetAlert<sup>X</sup> che hanno accesso alla rete (UNIFI, dhcp.leases , PiHole, ecc.). <br/> <br/> Nota: il tempo di scansione stesso dipende dal numero di indirizzi IP da controllare, quindi impostalo attentamente con la maschera di rete e l'interfaccia appropriate.",
|
||||
"SCAN_SUBNETS_name": "Reti da scansionare",
|
||||
"SYSTEM_TITLE": "Informazioni sistema",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Sovrascrivi valore",
|
||||
"Setting_Override_Description": "L'abilitazione di questa opzione sovrascriverà il valore predefinito fornito dall'app con il valore specificato sopra.",
|
||||
"Settings_Metadata_Toggle": "Mostra/nascondi i metadati per l'impostazione specificata.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "新規デバイスの削除",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "これは <b>デバイスを削除</b> するメンテナンス設定です。有効にした場合(<code>0</code> で無効)、<b>オフライン</b> 状態のデバイスの内、<b>最終接続日時</b> が指定された時間より古いものは削除されます。<b>オフラインデバイス</b> を <code>X</code> 時間経過後に自動削除したい場合に使用してください。",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "オフラインデバイスを削除する",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "読み込まれたプラグイン。プラグインの追加はアプリケーションの速度を低下させる可能性があります。有効化が必要なプラグインの種類やスキャンオプションについては、<a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">プラグインのドキュメント</a> を参照してください。読み込まれなかったプラグインの設定は失われます。読み込まない設定にできるのは <code>無効化</code> されたプラグインのみです。",
|
||||
"LOADED_PLUGINS_name": "読み込まれたプラグイン",
|
||||
"LOG_LEVEL_description": "この設定により、より詳細なログ出力が有効になります。データベースへのイベント書き込みのデバッグに有用です。",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "ほとんどのネットワーク内スキャナー(ARP-SCAN、NMAP、NSLOOKUP、DIG)は、特定のネットワークインターフェースとサブネットをスキャンすることに依存しています。この設定に関するヘルプについては、<a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">サブネットのドキュメント</a> を確認してください。特にVLAN、サポートされているVLANの種類、ネットワークマスクとインターフェースの確認方法についてです。<br/><br/> ネットワーク内スキャナーの代替手段として、NetAlert<sup>X</sup> がネットワークにアクセスする必要のない他のデバイススキャナー/インポーター(UNIFI、dhcp.leases、PiHoleなど)を有効化できます。<br/><br/> 注:スキャン時間自体は確認するIPアドレス数に依存するため、適切なネットワークマスクとインターフェースで慎重に設定してください。",
|
||||
"SCAN_SUBNETS_name": "スキャン対象ネットワーク",
|
||||
"SYSTEM_TITLE": "システム情報",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "上書き値",
|
||||
"Setting_Override_Description": "このオプションを有効にすると、アプリが提供するデフォルト値が上記で指定された値で上書きされます。",
|
||||
"Settings_Metadata_Toggle": "指定された設定のメタデータを表示/非表示にする。",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Behold nye enheter for",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Hvilke plugins som skal lastes. Å legge til plugins kan gjøre programmet tregere. Les mer om hvilke plugins som må aktiveres, typer eller skannealternativer i <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugin dokumentasjonen</a>. Ulastede plugins vil miste innstillingene sine. Bare <code>deaktiverte</code> plugins kan lastes ut.",
|
||||
"LOADED_PLUGINS_name": "Lastede plugins",
|
||||
"LOG_LEVEL_description": "Denne innstillingen vil aktivere mer detaljert logging. Nyttig for feilsøking av hendelser som skrives inn i databasen.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "De fleste skannere på nettet (ARP-Scan, NMAP, NSlookup, Dig) er avhengige av å skanne spesifikke nettverksgrensesnitt og undernett. Sjekk <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">subnett dokumentasjonen</a> for hjelp på denne innstillingen, spesielt VLAN-er, hvilke VLAN-er som støttes, eller hvordan du kan finne ut nettverksmasken og grensesnittet ditt. <br/> <br/> Et alternativ til skannere på nettet er å aktivere noen andre enhetsskannere/importører som ikke er avhengige av Netalert<sup>X</sup> med tilgang til nettverket (UniFi, DHCP-Leaser, Pihole, osv.). <br/> <br/> Merk: Selve skanningstiden avhenger av antall IP -adresser som skal sjekkes, så sett dette opp nøye med riktig nettverksmaske og grensesnitt.",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "Systeminformasjon",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Overstyr verdi",
|
||||
"Setting_Override_Description": "Aktivering av dette alternativet vil overstyre en App som leveres standard-verdi med verdien som er spesifisert ovenfor.",
|
||||
"Settings_Metadata_Toggle": "Vis/skjul metadata for den gitte innstillingen.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Usuń nowe urządzenia po",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "To ustawienie konserwacyjne dotyczące <b>USUWANIA urządzeń</b>. Jeśli jest włączone (<code>0</code> oznacza wyłączone), urządzenia, które są <b>Offline</b> i których <b>ostatnie połączenie</b> miało miejsce wcześniej niż określona liczba godzin w tym ustawieniu, zostaną usunięte. Skorzystaj z tej opcji, jeśli chcesz automatycznie usuwać <b>urządzenia offline</b> po <code>X</code> godzinach braku aktywności.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Usuń urządzenia niedostępne po",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Które wtyczki mają zostać załadowane. Dodanie wtyczek może spowolnić działanie aplikacji. Więcej informacji o tym, które wtyczki należy włączyć, jakie są ich typy oraz dostępne opcje skanowania znajdziesz w <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">dokumentacji wtyczek</a>. Wtyczki, które nie zostaną załadowane, utracą swoje ustawienia. Tylko wtyczki oznaczone jako <code>disabled</code> mogą zostać pominięte przy ładowaniu.",
|
||||
"LOADED_PLUGINS_name": "Załadowane wtyczki",
|
||||
"LOG_LEVEL_description": "To ustawienie włącza bardziej szczegółowe logowanie. Przydatne do debugowania zdarzeń zapisywanych w bazie danych.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "Większość skanerów sieciowych (ARP-SCAN, NMAP, NSLOOKUP, DIG) polega na skanowaniu określonych interfejsów sieciowych i podsieci. Zapoznaj się z <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">dokumentacją podsieci</a>, aby uzyskać pomoc w konfiguracji tego ustawienia, szczególnie w kontekście VLAN-ów, jakie VLAN-y są obsługiwane, lub jak ustalić maskę sieciową i interfejs. <br/> <br/> Alternatywą dla skanerów sieciowych jest włączenie innych skanerów/importerów urządzeń, które nie wymagają, aby NetAlert<sup>X</sup> miał dostęp do sieci (np. UNIFI, dhcp.leases, PiHole itp.). <br/> <br/> Uwaga: Czas skanowania zależy od liczby adresów IP do sprawdzenia, dlatego skonfiguruj to ostrożnie, ustawiając odpowiednią maskę sieciową i interfejs.",
|
||||
"SCAN_SUBNETS_name": "Sieci do zeskanowania",
|
||||
"SYSTEM_TITLE": "Informacje o systemie",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Nadpisz wartość",
|
||||
"Setting_Override_Description": "Włączenie tej opcji spowoduje nadpisanie domyślnej wartości dostarczonej przez aplikację wartością określoną powyżej.",
|
||||
"Settings_Metadata_Toggle": "Pokaż/ukryj metadane dla danego ustawienia.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Manter novos dispositivos por",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Esta é uma configuração de manutenção <b>EXCLUINDO dispositivos</b>. Se habilitado (<code>0</code> está desabilitado), dispositivos que estão <b>Offline</b> e sua data e hora <b>Last Offline</b> são mais antigas que as horas especificadas nesta configuração, serão deletados. Use esta configuração se você quiser remover automaticamente <b>Dispositivos Offline</b> após <code>X</code> horas offline.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Eliminar dispositivos offline após",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Quais plugins carregar. Adicionar plugins pode deixar o aplicativo lento. Leia mais sobre quais plugins precisam ser habilitados, tipos ou opções de escaneamento na <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">documentação de plugins</a>. Plugins descarregados perderão as suas configurações. Somente plugins <code>desabilitados</code> podem ser descarregados.",
|
||||
"LOADED_PLUGINS_name": "Plugins carregados",
|
||||
"LOG_LEVEL_description": "Esta definição permite um registo mais detalhado. Útil para depurar eventos gravados na base de dados.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Remover novos dispostivos depois",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Isto é uma definição de manutenção <b>ELIMINAR dispositivos</b>. Se ativado (<code>0</code> é desativado), dispositivos que estão <b>Offline</b> e a sua data de <b>Última conexão</b> foi mais antigo que as horas especificadas nesta definição, será eliminado. Use esta definição se quer auto-eliminar <b>Dispositivos Offline</b> após <code>X</code> horas de estarem offline.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Apagar dispositivos offline após",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Quais plugins carregar. Adicionar plugins pode deixar a aplicação lenta. Leia mais sobre quais plugins precisam ser ativados, tipos ou opções de escaneamento na <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">documentação de plugins</a>. Plugins descarregados perderão as suas configurações. Somente plugins <code>desativados</code> podem ser descarregados.",
|
||||
"LOADED_PLUGINS_name": "Plugins carregados",
|
||||
"LOG_LEVEL_description": "Esta definição permite um registo mais detalhado. Útil para depurar eventos gravados na base de dados.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "A maior parte dos scanners on-network (ARP-SCAN, NMAP, NSLOOKUP, DIG) baseiam-se em scanear interfaces de rede específicas e subredes. Veja a <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentação de subredes</a> para ajudar com esta definição, especialmente VLANs, quais VLANs são suportadas, ou como descobrir a máscara de rede e a sua interface. <br/> <br/> Uma alternativa a scanners on-network é ativar outro scanner de dispositivos/importadores que não dependam do NetAlert<sup>X</sup> tenha acesso à rede (UNIFI, dhcp.leases, PiHole, etc.). <br/> <br/> Nota: O tempo de scaneamento em si depende do número de endereços de IP a verificar, por isso configure isto com cuidado com a máscara e interface de rede apropriadas.",
|
||||
"SCAN_SUBNETS_name": "Redes a scanear",
|
||||
"SYSTEM_TITLE": "Informação de Sistema",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Sobrescrever valor",
|
||||
"Setting_Override_Description": "Ativar esta opção irá sobrescrever o valor predefinido pela App com o valor especificado acima.",
|
||||
"Settings_Metadata_Toggle": "Mostrar/esconder metadados para definição especificada.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Удалить новые устройства после",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Это настройка обслуживания <b>УДАЛЕНИЕ устройств</b>. Если этот параметр включен (<code>0</code> отключен), устройства, которые находятся <b>в Offline</b> и их дата и время <b>последнего подключения</b> старше, чем часы, указанные в этом параметре. Используйте этот параметр, если вы хотите автоматически удалять <b>Offline устройства</b> после <code>X</code> часов отсутствия в сети.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Удалить устройства Offline после",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Какие плагины загружать. Добавление плагинов может замедлить работу приложения. Подробнее о том, какие плагины необходимо включить, их типах или параметрах сканирования, читайте в <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS \">Документация по плагинам</a>. Выгруженные плагины потеряют ваши настройки. Можно выгрузить только <code>отключенные</code> плагины.",
|
||||
"LOADED_PLUGINS_name": "Загруженные плагины",
|
||||
"LOG_LEVEL_description": "Этот параметр включит более подробное ведение журнала. Полезно для отладки записи событий в базу данных.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "Большинство сетевых сканеров (ARP-SCAN, NMAP, NSLOOKUP, DIG) полагаются на сканирование определенных сетевых интерфейсов и подсетей. Дополнительную информацию по этому параметру можно найти в <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">документации по подсетям</a>, особенно VLAN, какие VLAN поддерживаются или как разобраться в маске сети и своем интерфейсе. <br/> <br/> Альтернативой сетевым сканерам является включение некоторых других сканеров/импортеров устройств, которые не полагаются на NetAlert<sup>X</sup>, имеющий доступ к сети (UNIFI, dhcp.leases , PiHole и др.). <br/> <br/> Примечание. Само время сканирования зависит от количества проверяемых IP-адресов, поэтому тщательно настройте его, указав соответствующую маску сети и интерфейс.",
|
||||
"SCAN_SUBNETS_name": "Сети для сканирования",
|
||||
"SYSTEM_TITLE": "Системная информация",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Переопределить значение",
|
||||
"Setting_Override_Description": "Включение этой опции приведет к переопределению значения по умолчанию, предоставленного приложением, на значение, указанное выше.",
|
||||
"Settings_Metadata_Toggle": "Показать/скрыть метаданные для данного параметра.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "",
|
||||
"LOG_LEVEL_description": "",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Yeni Cihazları Silmeden Önce",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Bu bir bakım ayarıdır <b>Cihazları SİLME</b>. Etkinleştirildiğinde (<code>0</code> devre dışıdır), <b>Çevrimdışı</b> olan ve <b>Son Çevrimdışı</b> tarihi belirtilen saatten daha eski olan cihazlar silinecektir. Bu ayarı, <code>X</code> saat çevrimdışı olduktan sonra <b>Çevrimdışı Cihazlar</b>ı otomatik olarak silmek için kullanabilirsiniz.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Çevrimdışı Cihazları Silmeden Önce",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Hangi Eklentilerin Yükleneceği. Eklenti eklemek, uygulamanın hızını yavaşlatabilir. Hangi eklentilerin etkinleştirilmesi gerektiği, türler veya tarama seçenekleri hakkında daha fazla bilgi için <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">eklentiler belgelerini</a> okuyun. Yüklenmeyen eklentiler, ayarlarınızı kaybedecektir. Sadece <code>devre dışı bırakılmış</code> eklentiler yüklenebilir.",
|
||||
"LOADED_PLUGINS_name": "Yüklenen Eklentiler",
|
||||
"LOG_LEVEL_description": "Bu ayar, daha ayrıntılı günlüklemeyi etkinleştirecektir. Veritabanına yazılan olayları hata ayıklamak için faydalıdır.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "Видаліть нові пристрої після",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "Це налаштування обслуговування <b>ВИДАЛЕННЯ пристроїв</b>. Якщо ввімкнено (<code>0</code> вимкнено), пристрої, які <b>офлайн</b>, та їх <b>Останнє підключення</b> дата та час старіші за вказані години в цьому налаштуванні, будуть видалені. Використовуйте це налаштування, якщо ви хочете автоматично видаляти <b>офлайн-пристрої</b> після <code>X</code> годин перебування в мережі.",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "Видаліть офлайн-пристрої після",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "Які плагіни завантажити. Додавання плагінів може уповільнити роботу програми. Дізнайтеся більше про те, які плагіни потрібно ввімкнути, типи чи параметри сканування в <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS \">документи плагінів</a>. Вивантажені плагіни втратять налаштування. Лише <code>вимкнені</code> плагіни можна вивантажити.",
|
||||
"LOADED_PLUGINS_name": "Завантажені плагіни",
|
||||
"LOG_LEVEL_description": "Цей параметр увімкне докладніше журналювання. Корисно для налагодження запису подій у базу даних.",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "Більшість мережевих сканерів (ARP-SCAN, NMAP, NSLOOKUP, DIG) покладаються на сканування конкретних мережевих інтерфейсів і підмереж. Перегляньте <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">документацію підмереж</a>, щоб отримати допомогу щодо цього налаштування, особливо VLAN, які VLAN підтримуються або як визначити маску мережі та ваш інтерфейс. <br/> <br/> Альтернативою мережевим сканерам є ввімкнення деяких інших сканерів/імпортерів пристроїв, які не покладаються на доступ NetAlert<sup>X</sup> до мережі (UNIFI, dhcp.leases , PiHole тощо). <br/> <br/> Примітка. Сам час сканування залежить від кількості IP-адрес, які потрібно перевірити, тому ретельно налаштуйте це за допомогою відповідної маски мережі та інтерфейсу.",
|
||||
"SCAN_SUBNETS_name": "Мережі для сканування",
|
||||
"SYSTEM_TITLE": "Інформація Про систему",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "Перевизначати значення",
|
||||
"Setting_Override_Description": "Якщо ввімкнути цю опцію, значення за умовчанням, надане програмою, буде замінено значенням, указаним вище.",
|
||||
"Settings_Metadata_Toggle": "Показати/сховати метадані для вказаного параметра.",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "",
|
||||
"LOG_LEVEL_description": "",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
|
||||
@@ -387,6 +387,8 @@
|
||||
"HRS_TO_KEEP_NEWDEV_name": "小时后删除新设备",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "这是<b>删除设备</b>的维护设置。如果启用了这个设置(<code>0</code>是禁用),任何<b>上次连接</b>时间比设置里存的指定时间长的<b>离线</b>设备都会被删除。要是您想在<code>X</code>小时后自动删除<b>离线设备</b>,请用这个设置。",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "保留离线设备",
|
||||
"Header_PauseScans_Tooltip": "",
|
||||
"Header_ResumeScans_Tooltip": "",
|
||||
"LOADED_PLUGINS_description": "加载哪些插件。添加插件可能会降低应用程序的速度。在<a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">插件文档</a>中详细了解需要启用哪些插件、插件类型或扫描选项。卸载插件将丢失您的设置。只有<code>已禁用</code>的插件才能卸载。",
|
||||
"LOADED_PLUGINS_name": "已加载插件",
|
||||
"LOG_LEVEL_description": "此设置将启用更详细的日志记录。对于调试写入数据库的事件很有用。",
|
||||
@@ -645,6 +647,8 @@
|
||||
"SCAN_SUBNETS_description": "大多数网络扫描器(ARP-SCAN、NMAP、NSLOOKUP、DIG)依赖于扫描特定的网络接口和子网。查看<a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">子网文档</a>以获取有关此设置的帮助,尤其是 VLAN、支持哪些 VLAN,或者如何确定网络掩码和接口。<br/> <br/> 网络扫描器的替代方法是启用一些其他不依赖于 NetAlert<sup>X</sup> 访问网络的设备扫描器/导入器(UNIFI、dhcp.leases、PiHole 等)。<br/> <br/> 注意:扫描时间本身取决于要检查的 IP 地址数量,因此请使用适当的网络掩码和接口仔细设置。",
|
||||
"SCAN_SUBNETS_name": "待扫描网络",
|
||||
"SYSTEM_TITLE": "系统信息",
|
||||
"Scans_Paused": "",
|
||||
"Scans_Resumed": "",
|
||||
"Setting_Override": "覆盖值",
|
||||
"Setting_Override_Description": "启用此选项将用上面指定的值覆盖应用程序提供的默认值。",
|
||||
"Settings_Metadata_Toggle": "显示/隐藏给定设置的元数据。",
|
||||
|
||||
+126
-104
@@ -18,6 +18,7 @@
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
# Register NetAlertX modules
|
||||
@@ -25,7 +26,7 @@ import conf
|
||||
from const import fullConfPath, sql_new_devices
|
||||
from logger import mylog
|
||||
from helper import filePermissions
|
||||
from utils.datetime_utils import timeNowUTC
|
||||
from utils.datetime_utils import timeNowUTC, is_datetime_future, normalizeTimeStamp
|
||||
from app_state import updateState
|
||||
from api import update_api, check_activity, update_GUI_port
|
||||
from scan.session_events import process_scan
|
||||
@@ -97,6 +98,10 @@ def main():
|
||||
all_plugins = None
|
||||
pm = None
|
||||
|
||||
# Tracks the last "remaining minutes" value broadcast while paused, so we only
|
||||
# call updateState() when the displayed countdown minute actually changes.
|
||||
last_paused_minute_broadcast = None
|
||||
|
||||
# -- SETTINGS BACKWARD COMPATIBILITY START --
|
||||
# rename settings that have changed names due to code cleanup or migration to plugins
|
||||
renameSettings(Path(fullConfPath))
|
||||
@@ -122,113 +127,130 @@ def main():
|
||||
# Update API endpoints
|
||||
update_api(db, all_plugins, False)
|
||||
|
||||
# proceed if 1 minute passed
|
||||
if conf.last_scan_run + datetime.timedelta(minutes=1) < conf.loop_start_time:
|
||||
# last time any scan or maintenance/upkeep was run
|
||||
conf.last_scan_run = loop_start_time
|
||||
# Pause gate: skip the automatic scheduled-scan block below while paused.
|
||||
# Manually-triggered scans (handled by check_and_run_user_event() above) are unaffected.
|
||||
pause_until_dt = normalizeTimeStamp(updateState().pause_until)
|
||||
|
||||
# Header (also broadcasts last_scan_run to frontend via SSE / app_state.json)
|
||||
updateState("Process: Start",
|
||||
last_scan_run=loop_start_time.replace(microsecond=0).isoformat(),
|
||||
next_scan_time="")
|
||||
if pause_until_dt and is_datetime_future(pause_until_dt):
|
||||
remaining_minutes = math.ceil((pause_until_dt - timeNowUTC(as_string=False)).total_seconds() / 60)
|
||||
|
||||
# Timestamp
|
||||
startTime = loop_start_time
|
||||
startTime = startTime.replace(microsecond=0)
|
||||
if remaining_minutes != last_paused_minute_broadcast:
|
||||
updateState(f"Process: Paused for {remaining_minutes} min")
|
||||
last_paused_minute_broadcast = remaining_minutes
|
||||
|
||||
# Check if any plugins need to run on schedule
|
||||
pm.run_plugin_scripts("schedule")
|
||||
|
||||
# Compute the next scheduled run time AFTER schedule check (which updates last_next_schedule)
|
||||
# Only device_scanner plugins have meaningful next_scan times for user display
|
||||
scanner_prefixes = {p["unique_prefix"] for p in all_plugins if p.get("plugin_type") == "device_scanner"}
|
||||
scanner_next = [s.last_next_schedule for s in conf.mySchedules if s.service in scanner_prefixes]
|
||||
|
||||
# Get the earliest next scan time across all device scanners and broadcast.
|
||||
# updateState validates the value is in the future before storing/broadcasting.
|
||||
if scanner_next:
|
||||
next_scan_dt = min(scanner_next)
|
||||
updateState(next_scan_time=next_scan_dt.replace(microsecond=0).isoformat())
|
||||
|
||||
# determine run/scan type based on passed time
|
||||
# --------------------------------------------
|
||||
|
||||
# Runs plugin scripts which are set to run every time after a scans finished
|
||||
pm.run_plugin_scripts("always_after_scan")
|
||||
|
||||
# process all the scanned data into new devices
|
||||
processScan = updateState("Check scan").processScan
|
||||
mylog("debug", [f"[MAIN] processScan: {processScan}"])
|
||||
|
||||
if processScan is True:
|
||||
mylog("debug", "[MAIN] start processing scan results")
|
||||
process_scan(db)
|
||||
updateState("Scan processed", None, None, None, None, False)
|
||||
|
||||
# Name resolution
|
||||
# --------------------------------------------
|
||||
|
||||
# Check if new devices found (created by process_scan)
|
||||
sql.execute(sql_new_devices)
|
||||
newDevices = sql.fetchall()
|
||||
db.commitDB()
|
||||
|
||||
# If new devices were found, run all plugins registered to be run when new devices are found
|
||||
# Run these before name resolution so plugins like NSLOOKUP that are configured
|
||||
# for `on_new_device` can populate names used in the notifications below.
|
||||
if len(newDevices) > 0:
|
||||
pm.run_plugin_scripts("on_new_device")
|
||||
|
||||
# run plugins before notification processing (e.g. Plugins to discover device names)
|
||||
pm.run_plugin_scripts("before_name_updates")
|
||||
|
||||
# Resolve devices names (will pick up results from on_new_device plugins above)
|
||||
mylog("debug", "[Main] Resolve devices names")
|
||||
update_devices_names(pm)
|
||||
|
||||
# Notification handling
|
||||
# ----------------------------------------
|
||||
|
||||
# send all configured notifications
|
||||
final_json = get_notifications(db)
|
||||
|
||||
# Write the notifications into the DB
|
||||
notification = NotificationInstance(db)
|
||||
notificationObj = notification.create(final_json, "")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Run all enabled publisher gateways (notification delivery)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Design notes:
|
||||
# - The eve_PendingAlertEmail flag is only cleared *after* a notification is sent.
|
||||
# - If no notification is sent (HasNotifications == False), the flag stays set,
|
||||
# meaning the event may still trigger alerts later depending on user settings
|
||||
# (e.g. down-event reporting, delay timers, plugin conditions).
|
||||
# - A pending flag means “still under evaluation,” not “missed.”
|
||||
# It will clear automatically once its event is included in a sent alert.
|
||||
# ------------------------------------------------------------------------------
|
||||
if notificationObj.HasNotifications:
|
||||
pm.run_plugin_scripts("on_notification")
|
||||
notification.setAllProcessed()
|
||||
|
||||
# Only clear pending email flags and plugins_events once notifications are sent.
|
||||
notification.clearPendingEmailFlag()
|
||||
|
||||
else:
|
||||
# If there are no notifications to process,
|
||||
# we still need to clear all plugin events to prevent database growth if
|
||||
# no notification gateways are configured
|
||||
notification.clearPluginEvents()
|
||||
mylog("verbose", ["[Notification] No changes to report"])
|
||||
|
||||
# Commit SQL
|
||||
db.commitDB()
|
||||
|
||||
mylog("verbose", ["[MAIN] Process: Idle"])
|
||||
else:
|
||||
# do something
|
||||
# mylog('verbose', ['[MAIN] Waiting to start next loop'])
|
||||
updateState("Process: Idle")
|
||||
if last_paused_minute_broadcast is not None:
|
||||
# Pause expired naturally (not via /scan/resume) - clear it and resume normal state
|
||||
updateState("Process: Idle", pause_until="")
|
||||
last_paused_minute_broadcast = None
|
||||
|
||||
# proceed if 1 minute passed
|
||||
if conf.last_scan_run + datetime.timedelta(minutes=1) < conf.loop_start_time:
|
||||
# last time any scan or maintenance/upkeep was run
|
||||
conf.last_scan_run = loop_start_time
|
||||
|
||||
# Header (also broadcasts last_scan_run to frontend via SSE / app_state.json)
|
||||
updateState("Process: Start",
|
||||
last_scan_run=loop_start_time.replace(microsecond=0).isoformat(),
|
||||
next_scan_time="")
|
||||
|
||||
# Timestamp
|
||||
startTime = loop_start_time
|
||||
startTime = startTime.replace(microsecond=0)
|
||||
|
||||
# Check if any plugins need to run on schedule
|
||||
pm.run_plugin_scripts("schedule")
|
||||
|
||||
# Compute the next scheduled run time AFTER schedule check (which updates last_next_schedule)
|
||||
# Only device_scanner plugins have meaningful next_scan times for user display
|
||||
scanner_prefixes = {p["unique_prefix"] for p in all_plugins if p.get("plugin_type") == "device_scanner"}
|
||||
scanner_next = [s.last_next_schedule for s in conf.mySchedules if s.service in scanner_prefixes]
|
||||
|
||||
# Get the earliest next scan time across all device scanners and broadcast.
|
||||
# updateState validates the value is in the future before storing/broadcasting.
|
||||
if scanner_next:
|
||||
next_scan_dt = min(scanner_next)
|
||||
updateState(next_scan_time=next_scan_dt.replace(microsecond=0).isoformat())
|
||||
|
||||
# determine run/scan type based on passed time
|
||||
# --------------------------------------------
|
||||
|
||||
# Runs plugin scripts which are set to run every time after a scans finished
|
||||
pm.run_plugin_scripts("always_after_scan")
|
||||
|
||||
# process all the scanned data into new devices
|
||||
processScan = updateState("Check scan").processScan
|
||||
mylog("debug", [f"[MAIN] processScan: {processScan}"])
|
||||
|
||||
if processScan is True:
|
||||
mylog("debug", "[MAIN] start processing scan results")
|
||||
process_scan(db)
|
||||
updateState("Scan processed", None, None, None, None, False)
|
||||
|
||||
# Name resolution
|
||||
# --------------------------------------------
|
||||
|
||||
# Check if new devices found (created by process_scan)
|
||||
sql.execute(sql_new_devices)
|
||||
newDevices = sql.fetchall()
|
||||
db.commitDB()
|
||||
|
||||
# If new devices were found, run all plugins registered to be run when new devices are found
|
||||
# Run these before name resolution so plugins like NSLOOKUP that are configured
|
||||
# for `on_new_device` can populate names used in the notifications below.
|
||||
if len(newDevices) > 0:
|
||||
pm.run_plugin_scripts("on_new_device")
|
||||
|
||||
# run plugins before notification processing (e.g. Plugins to discover device names)
|
||||
pm.run_plugin_scripts("before_name_updates")
|
||||
|
||||
# Resolve devices names (will pick up results from on_new_device plugins above)
|
||||
mylog("debug", "[Main] Resolve devices names")
|
||||
update_devices_names(pm)
|
||||
|
||||
# Notification handling
|
||||
# ----------------------------------------
|
||||
|
||||
# send all configured notifications
|
||||
final_json = get_notifications(db)
|
||||
|
||||
# Write the notifications into the DB
|
||||
notification = NotificationInstance(db)
|
||||
notificationObj = notification.create(final_json, "")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Run all enabled publisher gateways (notification delivery)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Design notes:
|
||||
# - The eve_PendingAlertEmail flag is only cleared *after* a notification is sent.
|
||||
# - If no notification is sent (HasNotifications == False), the flag stays set,
|
||||
# meaning the event may still trigger alerts later depending on user settings
|
||||
# (e.g. down-event reporting, delay timers, plugin conditions).
|
||||
# - A pending flag means “still under evaluation,” not “missed.”
|
||||
# It will clear automatically once its event is included in a sent alert.
|
||||
# ------------------------------------------------------------------------------
|
||||
if notificationObj.HasNotifications:
|
||||
pm.run_plugin_scripts("on_notification")
|
||||
notification.setAllProcessed()
|
||||
|
||||
# Only clear pending email flags and plugins_events once notifications are sent.
|
||||
notification.clearPendingEmailFlag()
|
||||
|
||||
else:
|
||||
# If there are no notifications to process,
|
||||
# we still need to clear all plugin events to prevent database growth if
|
||||
# no notification gateways are configured
|
||||
notification.clearPluginEvents()
|
||||
mylog("verbose", ["[Notification] No changes to report"])
|
||||
|
||||
# Commit SQL
|
||||
db.commitDB()
|
||||
|
||||
mylog("verbose", ["[MAIN] Process: Idle"])
|
||||
else:
|
||||
# do something
|
||||
# mylog('verbose', ['[MAIN] Waiting to start next loop'])
|
||||
updateState("Process: Idle")
|
||||
|
||||
# WORKFLOWS handling
|
||||
# ----------------------------------------
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import threading
|
||||
import sys
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
# flake8: noqa: E402
|
||||
|
||||
@@ -18,6 +19,7 @@ from logger import mylog # noqa: E402 [flake8 lint suppression]
|
||||
from helper import get_setting_value, get_env_setting_value, getBuildTimeStampAndVersion # noqa: E402 [flake8 lint suppression]
|
||||
from db.db_helper import get_date_from_period # noqa: E402 [flake8 lint suppression]
|
||||
from app_state import updateState # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
from .graphql_endpoint import devicesSchema # noqa: E402 [flake8 lint suppression]
|
||||
from .history_endpoint import delete_online_history # noqa: E402 [flake8 lint suppression]
|
||||
@@ -82,6 +84,7 @@ from .openapi.schemas import ( # noqa: E402 [flake8 lint suppression]
|
||||
DeviceImportResponse, UpdateDeviceColumnRequest,
|
||||
LockDeviceFieldRequest, UnlockDeviceFieldsRequest,
|
||||
CopyDeviceRequest, TriggerScanRequest,
|
||||
PauseScanRequest, PauseScanResponse, ResumeScanResponse,
|
||||
OpenPortsRequest,
|
||||
OpenPortsResponse, WakeOnLanRequest,
|
||||
WakeOnLanResponse, TracerouteRequest,
|
||||
@@ -1168,6 +1171,44 @@ def api_trigger_scan(payload=None):
|
||||
return jsonify({"success": True, "message": f"Scan triggered for type: {scan_type}"}), 200
|
||||
|
||||
|
||||
@app.route("/scan/pause", methods=["POST"])
|
||||
@validate_request(
|
||||
operation_id="pause_scan_scheduler",
|
||||
summary="Pause Scan Scheduler",
|
||||
description="Pause the automatic scheduled scan loop for a number of minutes. "
|
||||
"Manually-triggered scans (e.g. /nettools/trigger-scan) are not affected.",
|
||||
request_model=PauseScanRequest,
|
||||
response_model=PauseScanResponse,
|
||||
tags=["nettools"],
|
||||
validation_error_code=400,
|
||||
auth_callable=is_authorized
|
||||
)
|
||||
def api_pause_scan(payload=None):
|
||||
minutes = payload.minutes
|
||||
|
||||
pause_until = (timeNowUTC(as_string=False) + timedelta(minutes=minutes)).replace(microsecond=0).isoformat()
|
||||
|
||||
updateState(f"Process: Paused for {minutes} min", pause_until=pause_until)
|
||||
|
||||
return jsonify({"success": True, "message": f"Scans paused for {minutes} minutes", "pause_until": pause_until}), 200
|
||||
|
||||
|
||||
@app.route("/scan/resume", methods=["POST"])
|
||||
@validate_request(
|
||||
operation_id="resume_scan_scheduler",
|
||||
summary="Resume Scan Scheduler",
|
||||
description="Clear any active scan pause and resume the automatic scan scheduler. Idempotent — "
|
||||
"succeeds even if scans were not paused.",
|
||||
response_model=ResumeScanResponse,
|
||||
tags=["nettools"],
|
||||
auth_callable=is_authorized
|
||||
)
|
||||
def api_resume_scan(payload=None):
|
||||
updateState("Process: Idle", pause_until="")
|
||||
|
||||
return jsonify({"success": True, "message": "Scans resumed", "pause_until": ""}), 200
|
||||
|
||||
|
||||
# def trigger_scan(scan_type):
|
||||
# """Trigger a network scan by adding it to the execution queue."""
|
||||
# if scan_type not in ["ARPSCAN", "NMAPDEV", "NMAP"]:
|
||||
|
||||
@@ -519,6 +519,26 @@ class TriggerScanResponse(BaseResponse):
|
||||
scan_type: Optional[str] = Field(None, description="Type of scan that was triggered")
|
||||
|
||||
|
||||
class PauseScanRequest(BaseModel):
|
||||
"""Request to pause the automatic scan scheduler for a number of minutes."""
|
||||
minutes: int = Field(
|
||||
...,
|
||||
ge=1,
|
||||
le=1440,
|
||||
description="Number of minutes to pause automatic scans for (1-1440)"
|
||||
)
|
||||
|
||||
|
||||
class PauseScanResponse(BaseResponse):
|
||||
"""Response for pausing the automatic scan scheduler."""
|
||||
pause_until: Optional[str] = Field(None, description="ISO timestamp scans are paused until")
|
||||
|
||||
|
||||
class ResumeScanResponse(BaseResponse):
|
||||
"""Response for resuming the automatic scan scheduler."""
|
||||
pause_until: Optional[str] = Field(None, description="Always empty; confirms the pause was cleared")
|
||||
|
||||
|
||||
class OpenPortsRequest(BaseModel):
|
||||
"""Request for getting open ports."""
|
||||
target: str = Field(
|
||||
|
||||
+14
-4
@@ -45,7 +45,8 @@ class app_state_class:
|
||||
appVersion=None,
|
||||
buildTimestamp=None,
|
||||
last_scan_run=None,
|
||||
next_scan_time=None
|
||||
next_scan_time=None,
|
||||
pause_until=None
|
||||
):
|
||||
"""
|
||||
Initialize the application state, optionally overwriting previous values.
|
||||
@@ -93,6 +94,7 @@ class app_state_class:
|
||||
self.buildTimestamp = previousState.get("buildTimestamp", "")
|
||||
self.last_scan_run = previousState.get("last_scan_run", "")
|
||||
self.next_scan_time = previousState.get("next_scan_time", "")
|
||||
self.pause_until = previousState.get("pause_until", "")
|
||||
else: # init first time values
|
||||
self.settingsSaved = 0
|
||||
self.settingsImported = 0
|
||||
@@ -107,6 +109,7 @@ class app_state_class:
|
||||
self.buildTimestamp = ""
|
||||
self.last_scan_run = ""
|
||||
self.next_scan_time = ""
|
||||
self.pause_until = ""
|
||||
|
||||
# Overwrite with provided parameters if supplied
|
||||
if settingsSaved is not None:
|
||||
@@ -148,6 +151,9 @@ class app_state_class:
|
||||
self.next_scan_time = next_scan_time
|
||||
else:
|
||||
self.next_scan_time = ""
|
||||
# "" explicitly clears the pause (resume); a truthy value sets/extends it
|
||||
if pause_until is not None:
|
||||
self.pause_until = pause_until
|
||||
# check for new version every hour and if currently not running new version
|
||||
if self.isNewVersion is False and self.isNewVersionChecked + 3600 < int(
|
||||
timeNowUTC(as_string=False).timestamp()
|
||||
@@ -182,7 +188,8 @@ class app_state_class:
|
||||
appVersion=self.appVersion,
|
||||
buildTimestamp=self.buildTimestamp,
|
||||
last_scan_run=self.last_scan_run,
|
||||
next_scan_time=self.next_scan_time
|
||||
next_scan_time=self.next_scan_time,
|
||||
pause_until=self.pause_until
|
||||
)
|
||||
except Exception as e:
|
||||
mylog("none", [f"[app_state] SSE broadcast: {e}"])
|
||||
@@ -202,7 +209,8 @@ def updateState(newState = None,
|
||||
appVersion=None,
|
||||
buildTimestamp=None,
|
||||
last_scan_run=None,
|
||||
next_scan_time=None):
|
||||
next_scan_time = None,
|
||||
pause_until = None):
|
||||
"""
|
||||
Convenience method to create or update the app state.
|
||||
|
||||
@@ -218,6 +226,7 @@ def updateState(newState = None,
|
||||
buildTimestamp (str, optional): Build timestamp.
|
||||
last_scan_run (str, optional): ISO timestamp of last backend scan run.
|
||||
next_scan_time (str, optional): ISO timestamp of next scheduled device_scanner run.
|
||||
pause_until (str, optional): ISO timestamp scans are paused until; "" clears the pause.
|
||||
|
||||
Returns:
|
||||
app_state_class: Updated state object.
|
||||
@@ -233,7 +242,8 @@ def updateState(newState = None,
|
||||
appVersion,
|
||||
buildTimestamp,
|
||||
last_scan_run,
|
||||
next_scan_time
|
||||
next_scan_time,
|
||||
pause_until
|
||||
)
|
||||
|
||||
|
||||
|
||||
+8
-2
@@ -16,6 +16,8 @@ from db.db_upgrade import (
|
||||
ensure_Settings,
|
||||
ensure_Indexes,
|
||||
ensure_mac_lowercase_triggers,
|
||||
ensure_dangling_parentmac_cleanup_trigger,
|
||||
cleanup_existing_dangling_parentmac,
|
||||
migrate_to_camelcase,
|
||||
migrate_timestamps_to_utc,
|
||||
)
|
||||
@@ -225,6 +227,9 @@ class DB:
|
||||
# Normalization triggers
|
||||
ensure_mac_lowercase_triggers(self.sql)
|
||||
|
||||
# Prevent/repair dangling devParentMAC references left by deleted devices
|
||||
cleanup_existing_dangling_parentmac(self.sql)
|
||||
|
||||
# Device history table + audit triggers
|
||||
ensure_deviceshistory_table(self.sql)
|
||||
ensure_deviceshistory_triggers(self.sql)
|
||||
@@ -240,9 +245,10 @@ class DB:
|
||||
AppEvent_obj(self)
|
||||
|
||||
# AppEvent_obj.drop_all_triggers() wipes every trigger in the DB
|
||||
# (including trg_devhist_*) as part of its clean-start routine.
|
||||
# Re-create the device history audit triggers here so they survive.
|
||||
# (including trg_devhist_* and trg_clear_dangling_parentmac_on_delete)
|
||||
# as part of its clean-start routine. Re-create them here so they survive.
|
||||
ensure_deviceshistory_triggers(self.sql)
|
||||
ensure_dangling_parentmac_cleanup_trigger(self.sql)
|
||||
self.commitDB()
|
||||
|
||||
def get_table_as_json(self, sqlQuery, parameters=None):
|
||||
|
||||
@@ -146,6 +146,74 @@ def ensure_mac_lowercase_triggers(sql):
|
||||
return False
|
||||
|
||||
|
||||
# Sentinel devParentMAC values that are never actual device references
|
||||
PARENT_MAC_SENTINELS = ("", "internet", "null")
|
||||
|
||||
|
||||
def ensure_dangling_parentmac_cleanup_trigger(sql):
|
||||
"""
|
||||
Ensures a trigger exists that clears devParentMAC/devParentMACSource on any
|
||||
device that referenced a device MAC which was just deleted, preventing
|
||||
dangling Parent Node references.
|
||||
|
||||
Note: this intentionally does NOT touch the NEWDEV_devParentMAC setting.
|
||||
Settings are sourced from app.conf and get re-imported verbatim on every
|
||||
restart (see importConfigs()), so a DB-only fix here would be silently
|
||||
reverted. Stale NEWDEV_devParentMAC values are instead guarded against at
|
||||
the point of use in create_new_devices() (server/scan/device_handling.py).
|
||||
"""
|
||||
try:
|
||||
sql.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='trigger' AND name='trg_clear_dangling_parentmac_on_delete'"
|
||||
)
|
||||
if not sql.fetchone():
|
||||
mylog("verbose", ["[db_upgrade] Creating trigger 'trg_clear_dangling_parentmac_on_delete'"])
|
||||
sql.execute("""
|
||||
CREATE TRIGGER trg_clear_dangling_parentmac_on_delete
|
||||
AFTER DELETE ON Devices
|
||||
FOR EACH ROW
|
||||
WHEN OLD.devMac IS NOT NULL AND OLD.devMac != ''
|
||||
BEGIN
|
||||
UPDATE Devices
|
||||
SET devParentMAC = '', devParentMACSource = ''
|
||||
WHERE LOWER(devParentMAC) = LOWER(OLD.devMac);
|
||||
END;
|
||||
""")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
mylog("none", [f"[db_upgrade] ERROR while ensuring dangling parentMAC trigger: {e}"])
|
||||
return False
|
||||
|
||||
|
||||
def cleanup_existing_dangling_parentmac(sql) -> bool:
|
||||
"""
|
||||
One-time/idempotent cleanup for installations that already have devParentMAC
|
||||
values pointing to a MAC no longer present in Devices. The delete trigger
|
||||
only prevents new dangling references going forward, so this repairs data
|
||||
left over from before the trigger existed.
|
||||
"""
|
||||
try:
|
||||
sentinel_list = ", ".join(f"'{v}'" for v in PARENT_MAC_SENTINELS)
|
||||
|
||||
sql.execute(f"""
|
||||
UPDATE Devices
|
||||
SET devParentMAC = '', devParentMACSource = ''
|
||||
WHERE devParentMAC IS NOT NULL
|
||||
AND LOWER(devParentMAC) NOT IN ({sentinel_list})
|
||||
AND LOWER(devParentMAC) NOT IN (SELECT LOWER(devMac) FROM Devices)
|
||||
""")
|
||||
if sql.rowcount > 0:
|
||||
mylog("verbose", [f"[db_upgrade] Cleared {sql.rowcount} dangling devParentMAC reference(s)"])
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
mylog("none", [f"[db_upgrade] ERROR while cleaning up dangling parentMAC references: {e}"])
|
||||
return False
|
||||
|
||||
|
||||
def ensure_views(sql) -> bool:
|
||||
"""
|
||||
Ensures required views exist.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
NetAlertX models package.
|
||||
|
||||
Contains domain models and instances for notifications, devices, events, etc.
|
||||
"""
|
||||
@@ -219,6 +219,35 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SCAN_PAUSE",
|
||||
"type": {
|
||||
"dataType": "integer",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [{ "type": "number" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxLength": 50,
|
||||
"default_value": 30,
|
||||
"options": [],
|
||||
"localized": [],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Scan pause duration"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "How long (in minutes) should scans be paused when the user clicks the Pause button. Accepts values from <code>1</code> to <code>1440</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "REFRESH",
|
||||
"type": {
|
||||
@@ -271,7 +300,7 @@
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Default number of items shown in tables per page, for example in teh Devices lists."
|
||||
"string": "Default number of items shown in tables per page, for example in the Devices lists."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ from models.device_instance import DeviceInstance
|
||||
from scan.name_resolution import NameResolver
|
||||
from scan.device_heuristics import guess_icon, guess_type
|
||||
from db.db_helper import sanitize_SQL_input, list_to_where, safe_int
|
||||
from db.db_upgrade import PARENT_MAC_SENTINELS
|
||||
from db.authoritative_handler import (
|
||||
get_overwrite_sql_clause,
|
||||
can_overwrite_field,
|
||||
@@ -730,6 +731,22 @@ def create_new_devices(db):
|
||||
mylog("debug", f"[New Devices] Collecting New Devices Query: {query}")
|
||||
current_scan_data = sql.execute(query).fetchall()
|
||||
|
||||
# Resolve the default Parent Node setting once and guard against it pointing
|
||||
# to a MAC that no longer exists (e.g. that device was since deleted) -
|
||||
# falling back to unset rather than seeding new devices with a dangling reference.
|
||||
default_parent_mac_setting = get_setting_value("NEWDEV_devParentMAC")
|
||||
if default_parent_mac_setting and default_parent_mac_setting.lower() not in PARENT_MAC_SENTINELS:
|
||||
existing_device_macs = {
|
||||
str(row[0]).lower() for row in sql.execute("SELECT devMac FROM Devices").fetchall() if row[0]
|
||||
}
|
||||
if default_parent_mac_setting.lower() not in existing_device_macs:
|
||||
mylog(
|
||||
"verbose",
|
||||
f"[New Devices] NEWDEV_devParentMAC '{default_parent_mac_setting}' no longer "
|
||||
"exists in Devices - treating as unset",
|
||||
)
|
||||
default_parent_mac_setting = ""
|
||||
|
||||
for row in current_scan_data:
|
||||
(
|
||||
scanMac,
|
||||
@@ -771,7 +788,7 @@ def create_new_devices(db):
|
||||
scanParentMAC
|
||||
if scanParentMAC and scanMac.lower() != "internet"
|
||||
else (
|
||||
get_setting_value("NEWDEV_devParentMAC")
|
||||
default_parent_mac_setting
|
||||
if scanMac.lower() != "internet"
|
||||
else "null"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_token():
|
||||
return get_setting_value("API_TOKEN")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
# --- /scan/pause ---
|
||||
|
||||
|
||||
@patch("api_server.api_server_start.updateState")
|
||||
def test_pause_scan_success(mock_update_state, client, api_token):
|
||||
"""Valid minutes value pauses scans and returns a future pause_until timestamp."""
|
||||
mock_update_state.return_value = MagicMock()
|
||||
|
||||
response = client.post("/scan/pause", json={"minutes": 10}, headers=auth_headers(api_token))
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data["success"] is True
|
||||
assert "pause_until" in data and data["pause_until"]
|
||||
|
||||
mock_update_state.assert_called_once()
|
||||
args, kwargs = mock_update_state.call_args
|
||||
assert args[0] == "Process: Paused for 10 min"
|
||||
assert kwargs["pause_until"] == data["pause_until"]
|
||||
|
||||
|
||||
@patch("api_server.api_server_start.updateState")
|
||||
def test_pause_scan_default_minutes_used(mock_update_state, client, api_token):
|
||||
"""The header button's default 10-minute pause request is accepted."""
|
||||
mock_update_state.return_value = MagicMock()
|
||||
|
||||
response = client.post("/scan/pause", json={"minutes": 10}, headers=auth_headers(api_token))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["success"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("minutes", [0, -5, 1441, "ten"])
|
||||
def test_pause_scan_invalid_minutes(client, api_token, minutes):
|
||||
"""Out-of-bounds or non-integer minutes values are rejected with a 400."""
|
||||
response = client.post("/scan/pause", json={"minutes": minutes}, headers=auth_headers(api_token))
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.get_json()
|
||||
assert data["success"] is False
|
||||
|
||||
|
||||
def test_pause_scan_missing_minutes(client, api_token):
|
||||
"""Missing 'minutes' field is rejected with a 400."""
|
||||
response = client.post("/scan/pause", json={}, headers=auth_headers(api_token))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.get_json()["success"] is False
|
||||
|
||||
|
||||
def test_pause_scan_requires_auth(client):
|
||||
"""Unauthenticated requests are rejected."""
|
||||
response = client.post("/scan/pause", json={"minutes": 10})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# --- /scan/resume ---
|
||||
|
||||
|
||||
@patch("api_server.api_server_start.updateState")
|
||||
def test_resume_scan_success(mock_update_state, client, api_token):
|
||||
"""Resume clears the pause and reports pause_until as empty."""
|
||||
mock_update_state.return_value = MagicMock()
|
||||
|
||||
response = client.post("/scan/resume", headers=auth_headers(api_token))
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data["success"] is True
|
||||
assert data["pause_until"] == ""
|
||||
|
||||
mock_update_state.assert_called_once_with("Process: Idle", pause_until="")
|
||||
|
||||
|
||||
@patch("api_server.api_server_start.updateState")
|
||||
def test_resume_scan_idempotent_when_not_paused(mock_update_state, client, api_token):
|
||||
"""Calling resume when scans are not paused still succeeds (idempotent)."""
|
||||
mock_update_state.return_value = MagicMock()
|
||||
|
||||
response = client.post("/scan/resume", headers=auth_headers(api_token))
|
||||
response2 = client.post("/scan/resume", headers=auth_headers(api_token))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response2.status_code == 200
|
||||
assert response.get_json()["success"] is True
|
||||
assert response2.get_json()["success"] is True
|
||||
|
||||
|
||||
def test_resume_scan_requires_auth(client):
|
||||
"""Unauthenticated requests are rejected."""
|
||||
response = client.post("/scan/resume")
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Unit tests for dangling devParentMAC cleanup.
|
||||
|
||||
Tests verify that:
|
||||
- Deleting a device clears devParentMAC/devParentMACSource on devices that
|
||||
referenced it as their Parent Node.
|
||||
- Sentinel values ('', 'internet', 'null') are never touched.
|
||||
- Valid parent references are left untouched.
|
||||
- The one-time migration repairs pre-existing dangling data and is idempotent.
|
||||
|
||||
Note: the NEWDEV_devParentMAC *setting* is intentionally NOT handled here.
|
||||
Settings are sourced from app.conf and get re-imported verbatim on every
|
||||
restart, so a DB-only fix would be silently reverted. That case is instead
|
||||
guarded against at the point of use in create_new_devices() — see
|
||||
test/scan/test_field_lock_scan_integration.py.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
import sqlite3
|
||||
import tempfile
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from db.db_upgrade import ( # noqa: E402
|
||||
ensure_dangling_parentmac_cleanup_trigger,
|
||||
cleanup_existing_dangling_parentmac,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db():
|
||||
"""Create a temporary database for testing"""
|
||||
fd, db_path = tempfile.mkstemp(suffix='.db')
|
||||
os.close(fd)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE Devices (
|
||||
devMac TEXT PRIMARY KEY COLLATE NOCASE,
|
||||
devParentMAC TEXT,
|
||||
devParentMACSource TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
yield cursor, conn
|
||||
|
||||
conn.close()
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
class TestDanglingParentMacTrigger:
|
||||
"""Test suite for the AFTER DELETE cleanup trigger"""
|
||||
|
||||
def test_trigger_clears_dependent_devices_on_delete(self, temp_db):
|
||||
cursor, conn = temp_db
|
||||
assert ensure_dangling_parentmac_cleanup_trigger(cursor) is True
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC, devParentMACSource) VALUES (?, ?, ?)",
|
||||
("aa:bb:cc:dd:ee:01", "", ""),
|
||||
)
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC, devParentMACSource) VALUES (?, ?, ?)",
|
||||
("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:01", "NEWDEV"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("DELETE FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:01",))
|
||||
conn.commit()
|
||||
|
||||
cursor.execute(
|
||||
"SELECT devParentMAC, devParentMACSource FROM Devices WHERE devMac = ?",
|
||||
("aa:bb:cc:dd:ee:02",),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
assert row == ("", "")
|
||||
|
||||
def test_trigger_ignores_unrelated_deletes(self, temp_db):
|
||||
cursor, conn = temp_db
|
||||
ensure_dangling_parentmac_cleanup_trigger(cursor)
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:01", "internet"),
|
||||
)
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:02", ""),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("DELETE FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:02",))
|
||||
conn.commit()
|
||||
|
||||
cursor.execute(
|
||||
"SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:01",)
|
||||
)
|
||||
assert cursor.fetchone() == ("internet",)
|
||||
|
||||
|
||||
class TestCleanupExistingDanglingParentMac:
|
||||
"""Test suite for the one-time/idempotent data repair migration"""
|
||||
|
||||
def test_cleanup_clears_dangling_reference(self, temp_db):
|
||||
cursor, conn = temp_db
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC, devParentMACSource) VALUES (?, ?, ?)",
|
||||
("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:99", "NEWDEV"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
assert cleanup_existing_dangling_parentmac(cursor) is True
|
||||
|
||||
cursor.execute(
|
||||
"SELECT devParentMAC, devParentMACSource FROM Devices WHERE devMac = ?",
|
||||
("aa:bb:cc:dd:ee:02",),
|
||||
)
|
||||
assert cursor.fetchone() == ("", "")
|
||||
|
||||
def test_cleanup_preserves_valid_and_sentinel_values(self, temp_db):
|
||||
cursor, conn = temp_db
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:01", ""),
|
||||
)
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:01"),
|
||||
)
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:03", "internet"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
cleanup_existing_dangling_parentmac(cursor)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:02",)
|
||||
)
|
||||
assert cursor.fetchone() == ("aa:bb:cc:dd:ee:01",)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:03",)
|
||||
)
|
||||
assert cursor.fetchone() == ("internet",)
|
||||
|
||||
def test_cleanup_is_idempotent(self, temp_db):
|
||||
cursor, conn = temp_db
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:99"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
assert cleanup_existing_dangling_parentmac(cursor) is True
|
||||
assert cleanup_existing_dangling_parentmac(cursor) is True
|
||||
|
||||
cursor.execute(
|
||||
"SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:02",)
|
||||
)
|
||||
assert cursor.fetchone() == ("",)
|
||||
@@ -16,11 +16,16 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub NetAlertX-specific modules so tests can run outside the container.
|
||||
# sys.modules.setdefault() is a no-op when the real module is already loaded,
|
||||
# so this is safe to run inside the container too.
|
||||
# These stubs are only placeholders for the duration of the `import ntfy`
|
||||
# below - they are popped from sys.modules again right after, so they don't
|
||||
# leak into other test files sharing the same pytest session (which would
|
||||
# otherwise shadow the real modules, e.g. models.notification_instance, for
|
||||
# every subsequent test).
|
||||
# ---------------------------------------------------------------------------
|
||||
_tmp_log = tempfile.mkdtemp()
|
||||
|
||||
_stubbed_module_names = []
|
||||
|
||||
|
||||
def _stub(name: str, **attrs):
|
||||
if name not in sys.modules:
|
||||
@@ -28,6 +33,7 @@ def _stub(name: str, **attrs):
|
||||
for k, v in attrs.items():
|
||||
setattr(mod, k, v)
|
||||
sys.modules[name] = mod
|
||||
_stubbed_module_names.append(name)
|
||||
|
||||
|
||||
_stub("pytz", timezone=lambda tz: tz)
|
||||
@@ -57,6 +63,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "server",
|
||||
import ntfy # noqa: E402
|
||||
from ntfy import build_custom_headers # noqa: E402
|
||||
|
||||
# `ntfy` has already resolved its module-level `from x import y` bindings at
|
||||
# this point, so removing these fake entries from sys.modules doesn't affect
|
||||
# it - it just stops them from shadowing the real modules for other test
|
||||
# files collected later in the same pytest session.
|
||||
for _name in _stubbed_module_names:
|
||||
sys.modules.pop(_name, None)
|
||||
|
||||
BUILT_IN = {"Title": "NetAlertX Notification", "Authorization": "Bearer secret"}
|
||||
|
||||
|
||||
|
||||
@@ -231,6 +231,66 @@ def test_create_new_devices_sets_sources(scan_db_for_new_devices):
|
||||
assert row["devVlanSource"] == "NEWDEV"
|
||||
|
||||
|
||||
def test_create_new_devices_ignores_dangling_newdev_parentmac(scan_db_for_new_devices):
|
||||
"""A stale NEWDEV_devParentMAC pointing to a since-deleted device is treated as unset,
|
||||
instead of seeding the new device with another dangling Parent Node reference."""
|
||||
cur = scan_db_for_new_devices.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO CurrentScan (
|
||||
scanMac, scanName, scanVendor, scanSourcePlugin, scanLastIP,
|
||||
scanSyncHubNode, scanParentMAC, scanParentPort,
|
||||
scanSite, scanSSID, scanType
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"aa:bb:cc:dd:ee:11",
|
||||
"DeviceTwo",
|
||||
"AcmeVendor",
|
||||
"ARPSCAN",
|
||||
"192.168.1.11",
|
||||
"",
|
||||
"", # no parent reported by the scan itself
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
),
|
||||
)
|
||||
scan_db_for_new_devices.commit()
|
||||
|
||||
settings = {
|
||||
"NEWDEV_devType": "default-type",
|
||||
# points to a MAC that does not (and never did, in this test) exist in Devices
|
||||
"NEWDEV_devParentMAC": "99:99:99:99:99:99",
|
||||
"NEWDEV_devOwner": "owner",
|
||||
"NEWDEV_devGroup": "group",
|
||||
"NEWDEV_devComments": "",
|
||||
"NEWDEV_devLocation": "",
|
||||
"NEWDEV_devCustomProps": "",
|
||||
"NEWDEV_devParentRelType": "uplink",
|
||||
"SYNC_node_name": "SYNCNODE",
|
||||
}
|
||||
|
||||
db = Mock()
|
||||
db.sql_connection = scan_db_for_new_devices
|
||||
db.sql = cur
|
||||
db.commitDB = scan_db_for_new_devices.commit
|
||||
|
||||
with patch.multiple(
|
||||
device_handling,
|
||||
get_setting_value=Mock(side_effect=lambda key: settings.get(key, "")),
|
||||
safe_int=Mock(return_value=0),
|
||||
):
|
||||
device_handling.create_new_devices(db)
|
||||
|
||||
row = cur.execute(
|
||||
"SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:11",)
|
||||
).fetchone()
|
||||
|
||||
assert row["devParentMAC"] == ""
|
||||
|
||||
|
||||
def test_scan_updates_newdev_device_name(scan_db, mock_device_handlers):
|
||||
"""Scanner discovers name for device with NEWDEV source."""
|
||||
cur = scan_db.cursor()
|
||||
|
||||
Reference in new issue
Block a user