Merge branch 'next_release' of https://github.com/netalertx/NetAlertX into next_release

This commit is contained in:
Jokob @NetAlertX
2026-05-18 02:45:51 +00:00
20 changed files with 810 additions and 119 deletions

View File

@@ -30,6 +30,7 @@ REPORT_DASHBOARD_URL='update_REPORT_DASHBOARD_URL_setting'
INTRNT_RUN='schedule'
ARPSCAN_RUN='schedule'
NSLOOKUP_RUN='before_name_updates'
DIGSCAN_RUN='before_name_updates'
AVAHISCAN_RUN='before_name_updates'
NBTSCAN_RUN='before_name_updates'

View File

@@ -36,7 +36,7 @@ docker run -d --rm --network=host \
> Runtime UID/GID: The image defaults to a service user `netalertx` (UID/GID 20211). A separate readonly lock owner also uses UID/GID 20211 for 004/005 immutability. You can override the runtime UID/GID at build (ARG) or run (`--user` / compose `user:`) but must align writable mounts (`/data`, `/tmp*`) and tmpfs `uid/gid` to that choice.
See alternative [docked-compose examples](https://docs.netalertx.com/DOCKER_COMPOSE).
See alternative [docker-compose examples](https://docs.netalertx.com/DOCKER_COMPOSE).
### Default ports

View File

@@ -62,6 +62,7 @@ Device-detecting plugins insert values into the `CurrentScan` database table. T
| `INTRNT` | [internet_ip](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_ip/) | 🔍 | Internet IP scanner | | |
| `INTRSPD` | [internet_speedtest](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_speedtest/) | ♻ | Internet speed test | | |
| `IPNEIGH` | [ipneigh](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/ipneigh/) | 🔍 | Scan ARP (IPv4) and NDP (IPv6) tables | | |
| `KEALSS` | [kea_api](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/kea_api/) | 🔍/🆎 | Pull lease data from the Kea DHCP API | | |
| `LUCIRPC` | [luci_import](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/luci_import/) | 🔍 | Import connected devices from OpenWRT | | |
| `MAINT` | [maintenance](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/maintenance/) | ⚙ | Maintenance of logs, etc. | | |
| `MQTT` | [_publisher_mqtt](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_mqtt/) | ▶️ | MQTT for synching to Home Assistant | | |

View File

@@ -56,9 +56,6 @@ function initializeSessionsDatatable (sessionsRows) {
if (!cellData.includes("missing event") && !cellData.includes("..."))
{
if (cellData.includes("+")) { // Check if timezone offset is present
cellData = cellData.split('+')[0]; // Remove timezone offset
}
// console.log(cellData);
result = localizeTimestamp(cellData);
} else

View File

@@ -10832,7 +10832,8 @@ if (typeof jQuery === 'undefined') {
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = $(selector === '#' ? [] : selector)
selector = selector === '#' ? [] : selector
var $parent = $(document).find(selector)
if (e) e.preventDefault()
@@ -11228,9 +11229,15 @@ if (typeof jQuery === 'undefined') {
// =================
var clickHandler = function (e) {
var href
var $this = $(this)
var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
var href = $this.attr('href')
if (href) {
href = href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
}
var target = $this.attr('data-target') || href
var $target = $(document).find(target)
if (!$target.hasClass('carousel')) return
var options = $.extend({}, $target.data(), $this.data())
var slideIndex = $this.attr('data-slide-to')
@@ -11420,7 +11427,7 @@ if (typeof jQuery === 'undefined') {
var target = $trigger.attr('data-target')
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
return $(target)
return $(document).find(target)
}
@@ -11502,7 +11509,7 @@ if (typeof jQuery === 'undefined') {
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = selector && $(selector)
var $parent = selector && $(document).find(selector)
return $parent && $parent.length ? $parent : $this.parent()
}
@@ -11961,7 +11968,10 @@ if (typeof jQuery === 'undefined') {
$(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
var $this = $(this)
var href = $this.attr('href')
var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
var target = $this.attr('data-target') ||
(href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
var $target = $(document).find(target)
var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
if ($this.is('a')) e.preventDefault()

View File

@@ -1842,11 +1842,16 @@
return globalLocale;
}
function isLocaleNameSane(name) {
// Prevent names that look like filesystem paths, i.e contain '/' or '\'
return name.match('^[^/\\\\]*$') != null;
}
function loadLocale(name) {
var oldLocale = null;
// TODO: Find a better way to register and load all the locales in Node
if (!locales[name] && (typeof module !== 'undefined') &&
module && module.exports) {
module && module.exports && isLocaleNameSane(name)) {
try {
oldLocale = globalLocale._abbr;
var aliasedRequire = require;
@@ -2294,7 +2299,7 @@
function preprocessRFC2822(s) {
// Remove comments and folding whitespace and replace multiple-spaces with a single space
return s.replace(/\([^)]*\)|[\n\t]/g, ' ').replace(/(\s\s+)/g, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
return s.replace(/\((?:(?!\().)*\)|[\n\t]/gs, ' ').replace(/(\s\s+)/g, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
function checkWeekday(weekdayStr, parsedInput, config) {

View File

@@ -66,8 +66,8 @@
"CustProps_cant_remove": "No es pot eliminar, es necessita una propietat mínim.",
"DAYS_TO_KEEP_EVENTS_description": "Això és una configuració de manteniment. Especifica el nombre de dies que es conservaran els esdeveniments. Els esdeveniments antics s'esborraran periòdicament. També aplica als esdeveniments dels Connectors (Plugins).",
"DAYS_TO_KEEP_EVENTS_name": "Esborrar esdeveniments més vells de",
"DEEP_SLEEP_description": "",
"DEEP_SLEEP_name": "",
"DEEP_SLEEP_description": "Redueix l'ús de la CPU ampliant els temps d'espera inactiu entre els cicles de processament. Quan està activat, les exploracions es poden retardar fins a 1 minut i la interfície d'usuari pot ser menys sensible.",
"DEEP_SLEEP_name": "Son profund",
"DISCOVER_PLUGINS_description": "Desactiva aquesta opció per accelerar la inicialització i l'estalvi de configuració. Quan està desactivat, els connectors no es descobreixen, i no podeu afegir nous connectors a la configuració <code>LOADED_PLUGINS</code>.",
"DISCOVER_PLUGINS_name": "Descobreix els plugins",
"DevDetail_Children_Title": "Relacions filles",
@@ -141,7 +141,7 @@
"DevDetail_SessionTable_Duration": "Durada",
"DevDetail_SessionTable_IP": "IP",
"DevDetail_SessionTable_Order": "Ordre",
"DevDetail_Shortcut_CurrentStatus": "Estat actual",
"DevDetail_Shortcut_CurrentStatus": "Estat",
"DevDetail_Shortcut_DownAlerts": "Aturar alertes",
"DevDetail_Shortcut_Presence": "Presència",
"DevDetail_Shortcut_Sessions": "Sessions",
@@ -250,7 +250,7 @@
"Device_TableHead_NetworkSite": "Network Site",
"Device_TableHead_Owner": "Propietari",
"Device_TableHead_ParentRelType": "Tipus de relació",
"Device_TableHead_Parent_MAC": "Node pare de xarxa",
"Device_TableHead_Parent_MAC": "Node pare",
"Device_TableHead_Port": "Port",
"Device_TableHead_PresentLastScan": "Presència",
"Device_TableHead_ReqNicsOnline": "Requereix NICs En línia",
@@ -346,7 +346,7 @@
"Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.",
"Gen_NetworkMask": "Màscara de xarxa",
"Gen_New": "Nou",
"Gen_No_Data": "",
"Gen_No_Data": "Sense dades",
"Gen_Offline": "Fora de línia",
"Gen_Okay": "Ok",
"Gen_Online": "En línia",
@@ -808,4 +808,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Actualitza el valor sota. Sigues curós de seguir el format anterior. <b>No hi ha validació.</b>",
"test_event_tooltip": "Deseu els canvis primer abans de comprovar la configuració."
}
}

View File

@@ -37,7 +37,7 @@
"BackDevDetail_Tools_WOL_error": "O comando NÃO foi executado.",
"BackDevDetail_Tools_WOL_okay": "O comando foi executado.",
"BackDevices_Arpscan_disabled": "Análise Arp Desativada",
"BackDevices_Arpscan_enabled": "Análise ARP Ativada",
"BackDevices_Arpscan_enabled": "Análise Arp Ativada",
"BackDevices_Backup_CopError": "A base da dados original não pode ser gravada.",
"BackDevices_Backup_Failed": "A copia de segurança foi parcialmente executada. O arquivo não pode ser criado ou está vazio.",
"BackDevices_Backup_okay": "A copia de segurança foi feita executado corretamente com o novo arquivo",
@@ -61,14 +61,14 @@
"BackDevices_Restore_okay": "Restauração executada com sucesso.",
"BackDevices_darkmode_disabled": "Modo Noturno Desativado",
"BackDevices_darkmode_enabled": "Modo Noturno Ativado",
"CLEAR_NEW_FLAG_description": "Se ativado (<code>0</code> está desativado), dispositivos marcados como<b>Novo Dispositivo</b> serão desmarcados se o limite (especificado em horas) exceder o tempo da <b>Primeira Sessão </b>.",
"CLEAR_NEW_FLAG_description": "Se ativado (<code>0</code> está desativado), dispositivos marcados como <b>Novo Dispositivo</b> serão desmarcados se o limite (especificado em horas) exceder o tempo da <b>Primeira Sessão</b>.",
"CLEAR_NEW_FLAG_name": "Limpar a flag nova",
"CustProps_cant_remove": "Não é possível remover, é necessária pelo menos uma propriedade.",
"DAYS_TO_KEEP_EVENTS_description": "Esta é uma definição de manutenção. Especifica o número de dias de entradas de eventos que serão mantidas. Todos os eventos mais antigos serão apagados periodicamente. Também se aplica ao Histórico de eventos do plug-in.",
"DAYS_TO_KEEP_EVENTS_name": "Apagar eventos mais antigos que",
"DEEP_SLEEP_description": "Diminui a utilização do CPU ao prolongar tempos de espera ociosos entre ciclos de processamento. Quando ativo, análises podem ser atrasadas por até 1 minuto e o UI pode ficar menos responsivo.",
"DEEP_SLEEP_name": "Sleep profundo",
"DISCOVER_PLUGINS_description": "Desative esta opção para acelerar a inicialização e a gravação de definições. Quando desativada, os plug-ins não são descobertos e não é possível adicionar novos plug-ins à definição<code>LOADED_PLUGINS</code>.",
"DISCOVER_PLUGINS_description": "Desative esta opção para acelerar a inicialização e a gravação de definições. Quando desativada, os plug-ins não são descobertos e não é possível adicionar novos plug-ins à definição <code>LOADED_PLUGINS</code>.",
"DISCOVER_PLUGINS_name": "Descobrir plugins",
"DevDetail_Children_Title": "Relacionamentos de crianças",
"DevDetail_Copy_Device_Title": "Copiar pormenores do dispositivo",
@@ -98,7 +98,7 @@
"DevDetail_MainInfo_Location": "Localização",
"DevDetail_MainInfo_Name": "Nome",
"DevDetail_MainInfo_Network": "<i class=\"fa fa-server\"> </i> Node (MAC)",
"DevDetail_MainInfo_Network_Port": "<i class=\"fa fa-ethernet\"></i>Porta",
"DevDetail_MainInfo_Network_Port": "<i class=\"fa fa-ethernet\"></i> Porta",
"DevDetail_MainInfo_Network_Site": "Site",
"DevDetail_MainInfo_Network_Title": "Detalhes de Rede",
"DevDetail_MainInfo_Owner": "Proprietário",
@@ -341,7 +341,7 @@
"Gen_Filter": "Filtro",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Gerar",
"Gen_InvalidMac": "Endereço MAC Inválido.",
"Gen_InvalidMac": "Endereço Mac inválido.",
"Gen_Invalid_Value": "Um valor inválido foi inserido",
"Gen_LockedDB": "ERRO - A base de dados pode estar bloqueada - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.",
"Gen_NetworkMask": "Máscara de Rede",
@@ -350,7 +350,7 @@
"Gen_Offline": "Offline",
"Gen_Okay": "Ok",
"Gen_Online": "Online",
"Gen_Purge": "Purge",
"Gen_Purge": "Purgar",
"Gen_ReadDocs": "Leia mais em documentos.",
"Gen_Remove_All": "Remover tudo",
"Gen_Remove_Last": "Remover o último",
@@ -495,7 +495,7 @@
"Maintenance_Tool_upgrade_database_noti_text": "Tem certeza de que deseja atualizar a base de dados?<br>(talvez prefira arquivá-la)",
"Maintenance_Tool_upgrade_database_text": "Este botão atualizará a base de dados para ativar o gráfico Atividade de rede nas últimas 12 horas. Faça uma cópia de segurança da sua base de dados em caso de problemas.",
"Maintenance_Tools_Tab_BackupRestore": "Backup / Restauração",
"Maintenance_Tools_Tab_Logging": "Logs",
"Maintenance_Tools_Tab_Logging": "Registos",
"Maintenance_Tools_Tab_Settings": "Configurações",
"Maintenance_Tools_Tab_Tools": "Ferramentas",
"Maintenance_Tools_Tab_UISettings": "Configurações de interface",
@@ -503,7 +503,7 @@
"Maintenance_arp_status_off": "está atualmente desativado",
"Maintenance_arp_status_on": "Scan em curso",
"Maintenance_built_on": "Construído em",
"Maintenance_current_version": "Você está atualizado. Confira o que <a href=\"https://github.com/netalertx/NetAlertX/issues/138\" target=\"_blank\"> estou a trabalhar em</a>.",
"Maintenance_current_version": "Você está atualizado. Confira no que é que <a href=\"https://github.com/netalertx/NetAlertX/issues/138\" target=\"_blank\">estou a trabalhar em</a>.",
"Maintenance_database_backup": "Backups DB",
"Maintenance_database_backup_found": "foram encontrados backups",
"Maintenance_database_backup_total": "uso total do disco",
@@ -538,7 +538,7 @@
"Navigation_Report": "Reports enviados",
"Navigation_Settings": "Definições",
"Navigation_SystemInfo": "Informação de sistema",
"Navigation_Workflows": "Workflows",
"Navigation_Workflows": "Fluxos de Trabalho",
"Network_Assign": "Conectar ao nodo de network <i class=\"fa fa-server\"></i> em cima",
"Network_Cant_Assign": "Não é possível atribuir o node raiz da Internet como um node folha filho.",
"Network_Cant_Assign_No_Node_Selected": "Não é possível atribuir, nenhum node pai selecionado.",
@@ -565,13 +565,13 @@
"Network_ManageEdit_Name": "Novo nome de dispositivo",
"Network_ManageEdit_Name_text": "Nome sem caracteres especiais",
"Network_ManageEdit_Port": " Nova contagem de portas",
"Network_ManageEdit_Port_text": "Deixe em branco para Wi-Fi e Powerline.",
"Network_ManageEdit_Port_text": "Deixe em branco para Wi-Fi e Powerline",
"Network_ManageEdit_Submit": "Guardar Alterações",
"Network_ManageEdit_Type": "Novo tipo de dispositivo",
"Network_ManageEdit_Type_text": "-- Selecionar tipo --",
"Network_ManageLeaf": "Gerir atribuição",
"Network_ManageUnassign": "Cancelar Atribuição",
"Network_NoAssignedDevices": "Este nó de rede não tem quaisquer dispositivos atribuídos (nós folha). Atribua um abaixo ou vá ao separador <b><i class=\"fa fa-info-circle\"> Detalhes</b> em qualquer dispositivo em <a href=\"devices.php\"><b><i class=\"fa fa-laptop\"></i> Dispositivos</b></a>, e atribua-o a um <i <b><i class=\"fa fa-server\"></i> Nó de rede (MAC)</b> e <b><i class=\"fa fa-ethernet\"></i> Porta</b> lá.",
"Network_NoAssignedDevices": "Este nó de rede não tem quaisquer dispositivos atribuídos (nós folha). Atribua um abaixo ou vá à aba <b><i class=\"fa fa-info-circle\"></i> Detalhes</b> de qualquer dispositivo em <a href=\"devices.php\"><b> <i class=\"fa fa-laptop\"></i> Dispositivos</b></a>, e atribua os mesmos a uma rede <b><i class=\"fa fa-server\"></i> Nó (MAC)</b> e <b><i class=\"fa fa-ethernet\"></i> Porta</b> lá.",
"Network_NoDevices": "Sem dispositivos para configurar",
"Network_Node": "Nó de rede",
"Network_Node_Name": "Nome do nó",
@@ -777,35 +777,35 @@
"new_version_available": "Uma versão nova está disponível.",
"report_guid": "Guid de Notificação:",
"report_guid_missing": "Notificação associada não foi encontrada. Há um pequeno atraso entre notificações recentemente enviadas e as mesmas estarem disponíveis. Atualize a sua página e cache após alguns segundos. Também é possível que a notificação selecionada tenha sido eliminada durante a manutenção como especificado na definição <code>DBCLNP_NOTIFI_HIST</code>. <br/> <br/>Em vez disso, a última notificação é mostrada. A notificação em falta tem o seguinte GUID:",
"report_select_format": "",
"report_time": "",
"run_event_tooltip": "",
"select_icon_event_tooltip": "",
"settings_core_icon": "",
"settings_core_label": "",
"settings_device_scanners": "",
"settings_device_scanners_icon": "",
"settings_device_scanners_info": "",
"settings_device_scanners_label": "",
"settings_enabled": "",
"settings_enabled_icon": "",
"settings_expand_all": "",
"settings_imported": "",
"settings_imported_label": "",
"settings_missing": "",
"settings_missing_block": "",
"settings_old": "",
"settings_other_scanners": "",
"settings_other_scanners_icon": "",
"settings_other_scanners_label": "",
"settings_publishers": "",
"settings_publishers_icon": "",
"settings_publishers_info": "",
"settings_publishers_label": "",
"settings_readonly": "",
"settings_saved": "",
"settings_system_icon": "",
"settings_system_label": "",
"settings_update_item_warning": "",
"report_select_format": "Selecionar Formato:",
"report_time": "Tempo de notificação:",
"run_event_tooltip": "Ative a definição e guarde as suas mudanças primeiro antes de corrê-lo.",
"select_icon_event_tooltip": "Selecionar ícone",
"settings_core_icon": "fa-solid fa-gem",
"settings_core_label": "Core",
"settings_device_scanners": "Scaneadores de dispositivos usados para descobrir dispositivos que escrevem para a tabela da base de dados CurrentScan.",
"settings_device_scanners_icon": "fa-solid fa-magnifying-glass-plus",
"settings_device_scanners_info": "Carregar mais scaneadores de dispositivos com a definição <a href=\"/settings.php#LOADED_PLUGINS\">LOADED_PLUGINS</a>",
"settings_device_scanners_label": "Scaneadores de dispositivos",
"settings_enabled": "Definições ativas",
"settings_enabled_icon": "fa-solid fa-toggle-on",
"settings_expand_all": "Expandir todos",
"settings_imported": "Na última vez, as definições foram importadas a partir do ficheiro app.conf",
"settings_imported_label": "Definições importadas",
"settings_missing": "Nem todas as configurações foram carregadas! Carga elevada na base de dados ou na sequência de começo da aplicação. Clique no botão 🔄 de atualizar no topo.",
"settings_missing_block": "Erro: Definições não carregadas corretamente. Clique no botão 🔄 de atualizar no topo ou, alternativamente, verifique o registo do navegador para detalhes (F12).",
"settings_old": "A importar definições e a reinicializar…",
"settings_other_scanners": "Outros plugins de scaneadores que não são do dispositivo estão atualmente ativos.",
"settings_other_scanners_icon": "fa-solid fa-recycle",
"settings_other_scanners_label": "Outros scaneadores",
"settings_publishers": "Gateways de notificação ativados - editores que enviarão uma notificação de acordo com as suas definições.",
"settings_publishers_icon": "fa-solid fa-paper-plane",
"settings_publishers_info": "Carregar mais Editores com a definição <a href=\"/settings.php#LOADED_PLUGINS\">LOADED_PLUGINS</a>",
"settings_publishers_label": "Editores",
"settings_readonly": "Não foi possível LÊR ou ESCREVER na <code>app.conf</code>. Tente reiniciar o contentoer e ler a <a href=\"https://docs.netalertx.com/FILE_PERMISSIONS\" target=\"_blank\">documentação de permissões de ficheiro</a>",
"settings_saved": "<br/>Definições guardadas.<br/> A recarregar...<br/><i class=\"ion ion-ios-loop-strong fa-spin fa-2x fa-fw\"></i><br/>",
"settings_system_icon": "fa-solid fa-gear",
"settings_system_label": "Sistema",
"settings_update_item_warning": "Atualize o valor abaixo. Tenha cuidado em seguir o formato anterior. <b>Validação não é efetuada.</b>",
"test_event_tooltip": "Guarde as alterações antes de testar as definições."
}

View File

@@ -27,7 +27,7 @@
"AppEvents_ObjectType": "Тип объекта",
"AppEvents_Plugin": "Плагин",
"AppEvents_Type": "Тип",
"BACKEND_API_URL_description": "Используется для обеспечения связи между фронтендом и бэкендом. По умолчанию это значение установлено на <code>/server</code> и, как правило, не должно изменяться.",
"BACKEND_API_URL_description": "Используется для обеспечения связи между фронтендом и бэкендом. По умолчанию это значение установлено на <code>/server</code> и, как правило, не должно изменяться.",
"BACKEND_API_URL_name": "URL-адрес серверного API",
"BackDevDetail_Actions_Ask_Run": "Вы хотите выполнить действие?",
"BackDevDetail_Actions_Not_Registered": "Действие не зарегистрировано:· ",

View File

@@ -52,7 +52,7 @@
{ "elementType": "select", "elementOptions": [], "transformers": [] }
]
},
"default_value": "before_name_updates",
"default_value": "disabled",
"options": [
"disabled",
"before_name_updates",

View File

@@ -209,53 +209,83 @@ def execute_fping(timeout, args, all_devices, plugin_objects, subnets, interface
def run_fping(targets):
targets = expand_subnets(targets)
if not targets:
return []
is_ipv6 = any(':' in t for t in targets)
cmd = ["fping", "-a"] + args.split() + targets
if is_ipv6:
cmd.insert(1, "-6") # insert -6 after "fping"
ipv4_targets = [t for t in targets if ':' not in t]
ipv6_targets = [t for t in targets if ':' in t]
if interfaces:
cmd += ["-I", ",".join(interfaces)]
def run_family(family_targets, ipv6=False):
if not family_targets:
return []
mylog("verbose", [f"[{pluginName}] fping cmd: {' '.join(cmd)}"])
interface_list = interfaces if interfaces else [None]
try:
output = subprocess.check_output(
cmd,
stderr=subprocess.DEVNULL,
timeout=timeout,
text=True
)
except subprocess.CalledProcessError as e:
output = e.output
mylog("none", [f"[{pluginName}] fping returned non-zero exit code, reading alive hosts anyway"])
except subprocess.TimeoutExpired:
mylog("none", [f"[{pluginName}] fping timeout"])
return []
all_results = []
seen_ips = set()
results = []
for line in output.splitlines():
line = line.strip()
if not line:
continue
for interface in interface_list:
# Skip unreachable, timed out, or 100% packet loss
if "unreachable" in line.lower() or "timed out" in line.lower() or "100% loss" in line.lower():
mylog("debug", [f"[{pluginName}] fping skipping {line}"])
continue
cmd = ["fping", "-a"]
match = ip_regex.search(line)
if match:
ip = match.group(0)
mylog("debug", [f"[{pluginName}] adding {ip} from {line}"])
results.append((ip, line))
else:
mylog("verbose", [f"[{pluginName}] fping non-parseable {line}"])
if ipv6:
cmd.append("-6")
return results
cmd += args.split()
if interface:
cmd += ["-I", interface]
cmd += family_targets
mylog("verbose", [f"[{pluginName}] fping cmd: {' '.join(cmd)}"])
try:
output = subprocess.check_output(
cmd,
stderr=subprocess.DEVNULL,
timeout=timeout,
text=True
)
except subprocess.CalledProcessError as e:
output = e.output
mylog("none", [
f"[{pluginName}] fping returned non-zero exit code "
f"on interface {interface}, reading alive hosts anyway"
])
except subprocess.TimeoutExpired:
mylog("none", [
f"[{pluginName}] fping timeout on interface {interface}"
])
continue
for line in output.splitlines():
line = line.strip()
if not line:
continue
lower_line = line.lower()
if "unreachable" in lower_line or "timed out" in lower_line or "100% loss" in lower_line:
continue
match = ip_regex.search(line)
if match:
ip = match.group(0)
if ip in seen_ips:
continue
seen_ips.add(ip)
all_results.append((ip, line))
return all_results
return run_family(ipv4_targets, ipv6=False) + run_family(ipv6_targets, ipv6=True)
# Scan subnets
mylog("verbose", [f"[{pluginName}] run_fping: subnets {subnets}"])

99
front/plugins/kea_api/README.md Executable file
View File

@@ -0,0 +1,99 @@
## Overview
A plugin allowing for importing devices from the Kea DHCP API.
https://www.isc.org/kea/
And specifically:
https://kea.readthedocs.io/en/kea-2.6.3/api.html#lease4-get-all
### Usage
To enable the API, first you want to add something like this to your main kea configuration (this is for debian 13):
```json
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"hooks-libraries": [
{
"library": "/usr/lib/x86_64-linux-gnu/kea/hooks/libdhcp_lease_cmds.so"
}
],
```
And you need to install kea-ctrl-agent, with a config that looks something like this:
```json
{
"Control-agent": {
"http-host": "127.0.0.1",
"http-port": 8000,
"authentication": {
"type": "basic",
"realm": "Kea Control Agent",
"directory": "/etc/kea",
"clients": [
{
"user": "kea-api",
"password-file": "kea-api-password"
}
]
},
"control-sockets": {
"dhcp4": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
}
},
"loggers": [
{
"name": "kea-ctrl-agent",
"output-options": [
{
"output": "stdout",
"pattern": "%-5p %m\n"
}
],
"severity": "INFO",
"debuglevel": 0
}
]
}
}
```
You will need to configure the plugin with the URL to the API, and the username and password configured above (from kea-api-password file in the example)
#### Required Settings
These settings are required, besides the common device scanner settings:
- **Kea Control Agent URL** (`KEALSS_URL`): The full URL, including port number, to the Kea API.
- Default: `http://127.0.0.1:8000`
- This mirrors what you set up in the kea-ctrl-agent configuration.
- **Basic Auth Username** (`KEALSS_USER`): The user to use for authenticating with the Kea API.
- Default: `kea-api`
- This mirrors what you set up in the kea-ctrl-agent configuration.
- **Basic Auth Password** (`KEALSS_PASS`): The password to use for authenticating with the Kea API.
- This mirrors what you set up in the kea-ctrl-agent configuration.
- When using a password file, it should be the content of the password file.
### Notes
- This was tested on a basic Debian 13 install.
- When you install kea-ctrl-agent, it should ask you about creating a password.
- It's possible to run kea-ctrl-agent without password, but it's not recommended and at the moment we don't support that.
- I may provide some minimal support, if you ask nicely :)
- Version: 1.0.0
- Author: `void-spark`
- Release Date: `11/05/2026`

View File

@@ -0,0 +1,455 @@
{
"code_name": "kea_api",
"unique_prefix": "KEALSS",
"plugin_type": "device_scanner",
"execution_order" : "Layer_3",
"enabled": true,
"data_source": "script",
"data_filters": [
{
"compare_column": "objectPrimaryId",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"show_ui": true,
"localized": ["display_name", "description", "icon"],
"mapped_to_table": "CurrentScan",
"display_name": [{"language_code": "en_us", "string": "Kea DHCP API"}],
"icon": [{"language_code": "en_us", "string": "<i class=\"fa-solid fa-hourglass-half\"></i>"}],
"description": [{"language_code": "en_us", "string": "Imports leases via Kea Control Agent REST API"}],
"database_column_definitions": [
{
"column": "index",
"css_classes": "col-sm-2",
"show": true,
"type": "none",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Index"}]
},
{
"column": "objectPrimaryId",
"mapped_to_column": "scanMac",
"css_classes": "col-sm-2",
"show": true,
"type": "device_mac",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "MAC address"}]
},
{
"column": "objectSecondaryId",
"mapped_to_column": "scanLastIP",
"css_classes": "col-sm-2",
"show": true,
"type": "device_ip",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "IP" }]
},
{
"column": "dateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Created"}]
},
{
"column": "dateTimeChanged",
"mapped_to_column": "scanLastConnection",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Changed"}]
},
{
"column": "watchedValue1",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Is active"}]
},
{
"column": "watchedValue2",
"mapped_to_column": "scanName",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Hostname"}]
},
{
"column": "watchedValue4",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "State"}]
},
{
"column": "userData",
"css_classes": "col-sm-2",
"show": false,
"type": "textbox_save",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Comments"}]
},
{
"column": "Dummy",
"mapped_to_column": "scanSourcePlugin",
"mapped_to_column_data": {
"value": "KEALSS"
},
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Scan method"}]
},
{
"column": "status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value": "",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "Status"}]
}
],
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": {
"dataType": "string",
"elements": [{"elementType": "select", "elementOptions": [], "transformers": []}]
},
"default_value": "disabled",
"options": [
"disabled",
"once",
"schedule",
"always_after_scan",
"on_new_device"
],
"localized": ["name", "description"],
"name": [{"language_code": "en_us", "string": "When to run"}],
"description": [
{
"language_code": "en_us",
"string": "Enable import of devices from <code>Kea API</code>. If you select <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) or after you update your settings. ⚠ Use the same schedule if you have multiple <i class=\"fa-solid fa-magnifying-glass-plus\"></i> Device scanners enabled."
}
]
},
{
"function": "CMD",
"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [], "transformers": [] }
]
},
"default_value": "python3 /app/front/plugins/kea_api/script.py",
"options": [],
"localized": ["name", "description"],
"name": [{"language_code": "en_us", "string": "Command"}],
"description": [{"language_code": "en_us", "string": "Command to run"}]
},
{
"function": "URL",
"localized": ["name", "description"],
"name": [{"language_code": "en_us", "string": "API URL"}],
"description": [{"language_code": "en_us", "string": "Kea Control Agent URL"}],
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [],
"transformers": []
}
]
},
"default_value": "http://127.0.0.1:8000"
},
{
"function": "USER",
"localized": ["name", "description"],
"name": [{"language_code": "en_us", "string": "API User"}],
"description": [{"language_code": "en_us", "string": "Basic Auth Username"}],
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [],
"transformers": []
}
]
},
"default_value": "kea-api"
},
{
"function": "PASS",
"localized": ["name", "description"],
"name": [{"language_code": "en_us", "string": "API Password"}],
"description": [{"language_code": "en_us", "string": "Basic Auth Password"}],
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [{"type": "password"}],
"transformers": []
}
]
},
"default_value": ""
},
{
"function": "RUN_SCHD",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "span",
"elementOptions": [
{
"cssClasses": "input-group-addon validityCheck"
},
{
"getStringKey": "Gen_ValidIcon"
}
],
"transformers": []
},
{
"elementType": "input",
"elementOptions": [
{
"focusout": "validateRegex(this)"
},
{
"base64Regex": "Xig/OlwqfCg/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlsxLTldfFsxMl1bMC05XXwzWzAxXXxbMC05XSstWzAtOV0rfFwqL1swLTldKykpXHMrKD86XCp8KD86WzEtOV18MVswLTJdfFswLTldKy1bMC05XSt8XCovWzAtOV0rKSlccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XXxcKi9bMC05XSspKSQ="
}
],
"transformers": []
}
]
},
"default_value": "0 2 * * *",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Schedule"
}
],
"description": [
{
"language_code": "en_us",
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#KEALSS_RUN\"><code>KEALSS_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes. <br/> It's recommended to use the same schedule interval for all plugins responsible for discovering new devices."
}
]
},
{
"function": "RUN_TIMEOUT",
"type": {
"dataType": "integer",
"elements": [
{
"elementType": "input",
"elementOptions": [{ "type": "number" }],
"transformers": []
}
]
},
"default_value": 10,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
},
{
"function": "SET_ALWAYS",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "orderable": "true"}],
"transformers": []
}
]
},
"default_value": ["devMac", "devLastIP"],
"options": [
"devMac",
"devLastIP"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Set always columns"
}
],
"description": [
{
"language_code": "en_us",
"string": "These columns are treated as authoritative and will overwrite existing values, including those set by other plugins, unless the current value was explicitly set by the user (<code>Source = USER</code> or <code>Source = LOCKED</code>)."
}
]
},
{
"function": "SET_EMPTY",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
"transformers": []
}
]
},
"default_value": [],
"options": [
"devMac",
"devLastIP",
"devName",
"devSourcePlugin"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Set empty columns"
}
],
"description": [
{
"language_code": "en_us",
"string": "These columns are only overwritten if they are empty (<code>NULL</code> / empty string) or if their Source is set to <code>NEWDEV</code>"
}
]
},
{
"function": "WATCH",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "orderable": "true"}],
"transformers": []
}
]
},
"default_value": ["watchedValue1", "watchedValue4"],
"options": [
"watchedValue1",
"watchedValue2",
"watchedValue4"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Watched"
}
],
"description": [
{
"language_code": "en_us",
"string": "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>watchedValue1</code> is Active </li><li><code>watchedValue2</code> is Hostname </li><li><code>watchedValue4</code> is State</li></ul>"
}
]
},
{
"function": "REPORT_ON",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "orderable": "true"}],
"transformers": []
}
]
},
"default_value": ["new", "watched-changed"],
"options": [
"new",
"watched-changed",
"watched-not-changed",
"missing-in-last-scan"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Report on"
}
],
"description": [
{
"language_code": "en_us",
"string": "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>watchedValueN</code> columns changed."
}
]
}
]
}

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import os
import sys
import requests
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../server'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../plugins'))
from plugin_helper import Plugin_Objects, mylog, handleEmpty, is_mac
from helper import get_setting_value
from const import logPath
pluginName = 'KEALSS'
LOG_PATH = logPath + '/plugins'
LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
plugin_objects = Plugin_Objects(RESULT_FILE)
def main():
try:
url = get_setting_value(f'{pluginName}_URL')
user = get_setting_value(f'{pluginName}_USER')
password = get_setting_value(f'{pluginName}_PASS')
timeout = get_setting_value(f'{pluginName}_RUN_TIMEOUT')
mylog('verbose', [f'[{pluginName}] Querying Kea API at {url}'])
payload = {'command': 'lease4-get-all', 'service': ['dhcp4']}
response = requests.post(url, json=payload, auth=(user, password), timeout=max(1, timeout - 1))
response.raise_for_status()
data = response.json()
count = 0
for entry in data:
text = entry.get('text', '[API provided no text]')
# Result: 0 (success), 1 (error), or 3 (empty).
if entry['result'] == 0:
leases = entry['arguments']['leases']
for lease in leases:
mac = lease['hw-address']
state = lease['state']
if is_mac(mac):
plugin_objects.add_object(
primaryId = mac,
secondaryId = lease['ip-address'],
# Active or not, similar to watched1 of DHCPLSS plugin
watched1 = state == 0,
watched2 = lease['hostname'],
watched3 = None,
# Default (or assigned) (0), declined (1), expired-reclaimed (2), released (3), and registered (4)).
watched4 = state,
extra = None,
foreignKey = mac
)
count += 1
plugin_objects.write_result_file()
mylog('verbose', [f'[{pluginName}] Kea API response: {text}'])
mylog('verbose', [f'[{pluginName}] Successfully imported {count} devices reported by Kea API'])
elif entry['result'] == 1:
mylog('none', [f'[{pluginName}] ⚠ ERROR: Kea API indicated error: {text}'])
elif entry['result'] == 3:
mylog('verbose', [f'[{pluginName}] Kea API indicates no entries found: {text}'])
except Exception as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR: {str(e)}'])
if __name__ == '__main__':
main()

View File

@@ -613,7 +613,8 @@
"options": [
"devMac",
"devName",
"devVendor"
"devVendor",
"devLastIP"
],
"localized": ["name", "description"],
"name": [
@@ -810,7 +811,7 @@
"column": "Dummy",
"mapped_to_column": "scanSourcePlugin",
"mapped_to_column_data": {
"value": "sync"
"value": "SYNC"
},
"css_classes": "col-sm-2",
"show": false,

View File

@@ -185,9 +185,6 @@ def is_authorized():
return is_authorized_result
@app.route('/mcp/sse', methods=['GET', 'POST', 'OPTIONS'])
def api_mcp_sse():
if not is_authorized():

View File

@@ -1,13 +1,15 @@
import os
import base64
from flask import jsonify, request
from logger import mylog
from logger import mylog, Logger
from helper import get_setting_value
from utils.datetime_utils import timeNowUTC
from messaging.in_app import write_notification
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
# Make sure log level is initialized correctly
lggr = Logger(get_setting_value('LOG_LEVEL'))
def handle_sync_get():
"""Handle GET requests for SYNC (NODE → HUB)."""
@@ -28,7 +30,11 @@ def handle_sync_get():
response_data = base64.b64encode(raw_data).decode("utf-8")
write_notification("[Plugin: SYNC] Data sent", "info", timeNowUTC())
message = "[Plugin: SYNC] Data sent"
mylog('verbose', [message])
if lggr.isAbove('verbose'):
write_notification(message, 'info', timeNowUTC())
return jsonify({
"node_name": get_setting_value("SYNC_node_name"),
"status": 200,

View File

@@ -488,7 +488,7 @@ def importConfigs(pm, db, all_plugins):
# Plugins START
# -----------------
# necessary_plugins = ['UI', 'CUSTPROP', 'CLOUD' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS']
# necessary_plugins = ['UI', 'CUSTPROP', 'HEARTBEAT' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS']
necessary_plugins = [
"UI",
"CUSTPROP",

View File

@@ -35,11 +35,10 @@ from messaging.notification_sections import ( # noqa: E402 [flake8 lint suppres
)
import conf # noqa: E402 [flake8 lint suppression]
# ===============================================================================
# Timezone conversion
# ===============================================================================
def get_datetime_fields_from_columns(column_names):
return [
col for col in column_names
@@ -81,6 +80,7 @@ def apply_timezone(data, fields):
for field in fields:
value = row.get(field)
if not value:
continue
@@ -226,11 +226,24 @@ def get_notifications(db):
try:
json_obj = db.get_table_as_json(sqlQuery, parameters)
data = apply_timezone_to_json(json_obj, section)
except Exception as e:
mylog("minimal", [f"[Notification] DB error in section {section}: ", e])
mylog("none", [f"[Notification] apply_timezone failed for section {section}: ", e])
# fallback: preserve raw DB payload instead of dropping section
try:
data = json_obj.json.get("data", [])
except Exception:
data = []
final_json[section] = data
final_json[f"{section}_meta"] = {
"title": SECTION_TITLES.get(section, section),
"columnNames": getattr(json_obj, "columnNames", [])
}
continue
final_json[section] = json_obj.json.get("data", [])
final_json[section] = data
final_json[f"{section}_meta"] = {
"title": SECTION_TITLES.get(section, section),
"columnNames": getattr(json_obj, "columnNames", [])

View File

@@ -206,17 +206,18 @@ def format_date_iso(date_val: str) -> Optional[str]:
else:
dt = date_val
# 2. If it has no timezone, assume it's UTC (our DB storage format)
# then CONVERT to user's configured timezone
# 2. Normalize to UTC first, then convert to target timezone
if dt.tzinfo is None:
# Mark as UTC first — critical: localize() would label without converting
dt = dt.replace(tzinfo=datetime.UTC)
# Resolve target timezone; fall back to UTC if conf.tz is missing/invalid
try:
target_tz = conf.tz if isinstance(conf.tz, datetime.tzinfo) else ZoneInfo(conf.tz)
except (ZoneInfoNotFoundError, ValueError, TypeError):
target_tz = datetime.UTC
dt = dt.astimezone(target_tz)
else:
dt = dt.astimezone(datetime.UTC)
try:
target_tz = conf.tz if isinstance(conf.tz, datetime.tzinfo) else ZoneInfo(str(conf.tz))
except Exception:
target_tz = datetime.UTC
dt = dt.astimezone(target_tz)
# 3. Return the string. .isoformat() will now include the +11:00 or +10:00
return dt.isoformat()