diff --git a/back/app.conf b/back/app.conf index 95fcf24f..4add3c21 100755 --- a/back/app.conf +++ b/back/app.conf @@ -30,6 +30,7 @@ REPORT_DASHBOARD_URL='update_REPORT_DASHBOARD_URL_setting' INTRNT_RUN='schedule' ARPSCAN_RUN='schedule' NSLOOKUP_RUN='before_name_updates' +DIGSCAN_RUN='before_name_updates' AVAHISCAN_RUN='before_name_updates' NBTSCAN_RUN='before_name_updates' diff --git a/docs/DOCKER_INSTALLATION.md b/docs/DOCKER_INSTALLATION.md index 1c03e998..70183b10 100644 --- a/docs/DOCKER_INSTALLATION.md +++ b/docs/DOCKER_INSTALLATION.md @@ -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 diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index d108aeea..e8224fd2 100755 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -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 | | | diff --git a/front/deviceDetailsSessions.php b/front/deviceDetailsSessions.php index 837a8971..0d8c7664 100755 --- a/front/deviceDetailsSessions.php +++ b/front/deviceDetailsSessions.php @@ -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 diff --git a/front/lib/datatables/datatables.js b/front/lib/datatables/datatables.js index 2027407d..168f9fe8 100755 --- a/front/lib/datatables/datatables.js +++ b/front/lib/datatables/datatables.js @@ -10832,7 +10832,8 @@ if (typeof jQuery === 'undefined') { selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = $(selector === '#' ? [] : selector) + selector = selector === '#' ? [] : selector + var $parent = $(document).find(selector) if (e) e.preventDefault() @@ -11228,9 +11229,15 @@ if (typeof jQuery === 'undefined') { // ================= var clickHandler = function (e) { - var href var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + var href = $this.attr('href') + if (href) { + href = href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + } + + var target = $this.attr('data-target') || href + var $target = $(document).find(target) + if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') @@ -11420,7 +11427,7 @@ if (typeof jQuery === 'undefined') { var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - return $(target) + return $(document).find(target) } @@ -11502,7 +11509,7 @@ if (typeof jQuery === 'undefined') { selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = selector && $(selector) + var $parent = selector && $(document).find(selector) return $parent && $parent.length ? $parent : $this.parent() } @@ -11961,7 +11968,10 @@ if (typeof jQuery === 'undefined') { $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var target = $this.attr('data-target') || + (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + + var $target = $(document).find(target) var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() diff --git a/front/lib/moment/moment.js b/front/lib/moment/moment.js index 1b129716..a7eb86de 100755 --- a/front/lib/moment/moment.js +++ b/front/lib/moment/moment.js @@ -1842,11 +1842,16 @@ return globalLocale; } + function isLocaleNameSane(name) { + // Prevent names that look like filesystem paths, i.e contain '/' or '\' + return name.match('^[^/\\\\]*$') != null; + } + function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node if (!locales[name] && (typeof module !== 'undefined') && - module && module.exports) { + module && module.exports && isLocaleNameSane(name)) { try { oldLocale = globalLocale._abbr; var aliasedRequire = require; @@ -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) { diff --git a/front/php/templates/language/ca_ca.json b/front/php/templates/language/ca_ca.json index 807ab146..4e28136c 100644 --- a/front/php/templates/language/ca_ca.json +++ b/front/php/templates/language/ca_ca.json @@ -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ó LOADED_PLUGINS.", "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. No hi ha validació.", "test_event_tooltip": "Deseu els canvis primer abans de comprovar la configuració." -} \ No newline at end of file +} diff --git a/front/php/templates/language/pt_pt.json b/front/php/templates/language/pt_pt.json index 15aca397..a5629325 100644 --- a/front/php/templates/language/pt_pt.json +++ b/front/php/templates/language/pt_pt.json @@ -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 (0 está desativado), dispositivos marcados comoNovo Dispositivo serão desmarcados se o limite (especificado em horas) exceder o tempo da Primeira Sessão .", + "CLEAR_NEW_FLAG_description": "Se ativado (0 está desativado), dispositivos marcados como Novo Dispositivo serão desmarcados se o limite (especificado em horas) exceder o tempo da Primeira Sessão.", "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çãoLOADED_PLUGINS.", + "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 LOADED_PLUGINS.", "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": " Node (MAC)", - "DevDetail_MainInfo_Network_Port": "Porta", + "DevDetail_MainInfo_Network_Port": " 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?
(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 estou a trabalhar em.", + "Maintenance_current_version": "Você está atualizado. Confira no que é que estou a trabalhar em.", "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 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 Detalhes em qualquer dispositivo em Dispositivos, e atribua-o a um Nó de rede (MAC) e Porta lá.", + "Network_NoAssignedDevices": "Este nó de rede não tem quaisquer dispositivos atribuídos (nós folha). Atribua um abaixo ou vá à aba Detalhes de qualquer dispositivo em Dispositivos, e atribua os mesmos a uma rede Nó (MAC) e Porta 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 DBCLNP_NOTIFI_HIST.

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 LOADED_PLUGINS", + "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 LOADED_PLUGINS", + "settings_publishers_label": "Editores", + "settings_readonly": "Não foi possível LÊR ou ESCREVER na app.conf. Tente reiniciar o contentoer e ler a documentação de permissões de ficheiro", + "settings_saved": "
Definições guardadas.
A recarregar...

", + "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. Validação não é efetuada.", "test_event_tooltip": "Guarde as alterações antes de testar as definições." } diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json index eb2acbe3..4d6b1732 100644 --- a/front/php/templates/language/ru_ru.json +++ b/front/php/templates/language/ru_ru.json @@ -27,7 +27,7 @@ "AppEvents_ObjectType": "Тип объекта", "AppEvents_Plugin": "Плагин", "AppEvents_Type": "Тип", - "BACKEND_API_URL_description": "Используется для обеспечения связи между фронтендом и бэкендом. По умолчанию это значение установлено на /server и, как правило, не должно изменяться.", + "BACKEND_API_URL_description": "Используется для обеспечения связи между фронтендом и бэкендом. По умолчанию это значение установлено на /server и, как правило, не должно изменяться.", "BACKEND_API_URL_name": "URL-адрес серверного API", "BackDevDetail_Actions_Ask_Run": "Вы хотите выполнить действие?", "BackDevDetail_Actions_Not_Registered": "Действие не зарегистрировано:· ", diff --git a/front/plugins/dig_scan/config.json b/front/plugins/dig_scan/config.json index fb39b55d..9c176e7f 100755 --- a/front/plugins/dig_scan/config.json +++ b/front/plugins/dig_scan/config.json @@ -52,7 +52,7 @@ { "elementType": "select", "elementOptions": [], "transformers": [] } ] }, - "default_value": "before_name_updates", + "default_value": "disabled", "options": [ "disabled", "before_name_updates", diff --git a/front/plugins/icmp_scan/icmp.py b/front/plugins/icmp_scan/icmp.py index e5955cd1..91be5311 100755 --- a/front/plugins/icmp_scan/icmp.py +++ b/front/plugins/icmp_scan/icmp.py @@ -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}"]) diff --git a/front/plugins/kea_api/README.md b/front/plugins/kea_api/README.md new file mode 100755 index 00000000..9ddc174c --- /dev/null +++ b/front/plugins/kea_api/README.md @@ -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` diff --git a/front/plugins/kea_api/config.json b/front/plugins/kea_api/config.json new file mode 100644 index 00000000..6bc00f3d --- /dev/null +++ b/front/plugins/kea_api/config.json @@ -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": ""}], + "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": "
" + }, + { + "equals": "watched-changed", + "replacement": "
" + }, + { + "equals": "new", + "replacement": "
" + }, + { + "equals": "missing-in-last-scan", + "replacement": "
" + } + ], + "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 Kea API. If you select schedule the scheduling settings from below are applied. If you select once 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 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 schedule in the KEALSS_RUN setting. Make sure you enter the schedule in the correct cron-like format (e.g. validate at crontab.guru). For example entering 0 4 * * * will run the scan after 4 am in the TIMEZONE you set above. Will be run NEXT time the time passes.
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 (Source = USER or Source = LOCKED)." + } + ] + }, + { + "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 (NULL / empty string) or if their Source is set to NEWDEV" + } + ] + }, + { + "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 CTRL + Click to select/deselect.
  • watchedValue1 is Active
  • watchedValue2 is Hostname
  • watchedValue4 is State
" + } + ] + }, + { + "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. new means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. watched-changed means that selected watchedValueN columns changed." + } + ] + } + ] +} \ No newline at end of file diff --git a/front/plugins/kea_api/script.py b/front/plugins/kea_api/script.py new file mode 100644 index 00000000..9dc72d8c --- /dev/null +++ b/front/plugins/kea_api/script.py @@ -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() diff --git a/front/plugins/sync/config.json b/front/plugins/sync/config.json index 7179071b..e364c339 100755 --- a/front/plugins/sync/config.json +++ b/front/plugins/sync/config.json @@ -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, diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py index 484e19f0..f1342397 100755 --- a/server/api_server/api_server_start.py +++ b/server/api_server/api_server_start.py @@ -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(): diff --git a/server/api_server/sync_endpoint.py b/server/api_server/sync_endpoint.py index 23529161..51d1704b 100755 --- a/server/api_server/sync_endpoint.py +++ b/server/api_server/sync_endpoint.py @@ -1,13 +1,15 @@ import os import base64 from flask import jsonify, request -from logger import mylog +from logger import mylog, Logger from helper import get_setting_value from utils.datetime_utils import timeNowUTC from messaging.in_app import write_notification INSTALL_PATH = os.getenv("NETALERTX_APP", "/app") +# Make sure log level is initialized correctly +lggr = Logger(get_setting_value('LOG_LEVEL')) def handle_sync_get(): """Handle GET requests for SYNC (NODE → HUB).""" @@ -28,7 +30,11 @@ def handle_sync_get(): response_data = base64.b64encode(raw_data).decode("utf-8") - write_notification("[Plugin: SYNC] Data sent", "info", timeNowUTC()) + message = "[Plugin: SYNC] Data sent" + mylog('verbose', [message]) + if lggr.isAbove('verbose'): + write_notification(message, 'info', timeNowUTC()) + return jsonify({ "node_name": get_setting_value("SYNC_node_name"), "status": 200, diff --git a/server/initialise.py b/server/initialise.py index 9509b06b..bf53c494 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -488,7 +488,7 @@ def importConfigs(pm, db, all_plugins): # Plugins START # ----------------- - # necessary_plugins = ['UI', 'CUSTPROP', 'CLOUD' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS'] + # necessary_plugins = ['UI', 'CUSTPROP', 'HEARTBEAT' ,'DBCLNP', 'INTRNT','MAINT','NEWDEV', 'SETPWD', 'SYNC', 'VNDRPDT', 'WORKFLOWS'] necessary_plugins = [ "UI", "CUSTPROP", diff --git a/server/messaging/reporting.py b/server/messaging/reporting.py index dfe9f087..52cbf475 100755 --- a/server/messaging/reporting.py +++ b/server/messaging/reporting.py @@ -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", []) diff --git a/server/utils/datetime_utils.py b/server/utils/datetime_utils.py index df125b39..4366e7c0 100644 --- a/server/utils/datetime_utils.py +++ b/server/utils/datetime_utils.py @@ -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()