diff --git a/Dockerfile b/Dockerfile
index 6a044d0d..dbf63f7e 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -40,7 +40,7 @@ ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
RUN apk update --no-cache \
&& apk add --no-cache bash zip lsblk gettext-envsubst sudo mtr tzdata s6-overlay \
- && apk add --no-cache curl arp-scan iproute2 iproute2-ss nmap nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
+ && apk add --no-cache curl arp-scan iproute2 iproute2-ss nmap nmap-scripts traceroute nbtscan avahi avahi-tools openrc dbus net-tools net-snmp-tools bind-tools awake ca-certificates \
&& apk add --no-cache sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session \
&& apk add --no-cache python3 nginx \
&& apk add --no-cache dcron \
diff --git a/Dockerfile.debian b/Dockerfile.debian
index 128c26d7..42f6e910 100755
--- a/Dockerfile.debian
+++ b/Dockerfile.debian
@@ -33,7 +33,7 @@ COPY --chmod=775 --chown=${USER_ID}:${USER_GID} . ${INSTALL_DIR}/
RUN apt-get install -y \
tini snmp ca-certificates curl libwww-perl arp-scan perl apt-utils cron sudo \
nginx-light php php-cgi php-fpm php-sqlite3 php-curl sqlite3 dnsutils net-tools php-openssl \
- python3 python3-dev iproute2 nmap python3-pip zip systemctl usbutils traceroute nbtscan
+ python3 python3-dev iproute2 nmap python3-pip zip systemctl usbutils traceroute nbtscan avahi avahi-tools openrc dbus
# Alternate dependencies
RUN apt-get install nginx nginx-core mtr php-fpm php8.2-fpm php-cli php8.2 php8.2-sqlite3 -y
diff --git a/back/app.conf b/back/app.conf
index c122d021..8e1e5d8d 100755
--- a/back/app.conf
+++ b/back/app.conf
@@ -19,7 +19,7 @@
SCAN_SUBNETS=['192.168.1.0/24 --interface=eth0']
TIMEZONE='Europe/Berlin'
-LOADED_PLUGINS = ['ARPSCAN','CSVBCKP','DBCLNP', 'INTRNT','MAINT','NEWDEV','NSLOOKUP','NTFPRCS', 'PHOLUS','SETPWD','SMTP', 'SYNC', 'VNDRPDT', 'WORKFLOWS']
+LOADED_PLUGINS = ['ARPSCAN','CSVBCKP','DBCLNP', 'INTRNT','MAINT','NEWDEV','NSLOOKUP','NTFPRCS', 'AVAHISCAN', 'PHOLUS','SETPWD','SMTP', 'SYNC', 'VNDRPDT', 'WORKFLOWS']
DAYS_TO_KEEP_EVENTS=90
# Used for generating links in emails. Make sure not to add a trailing slash!
diff --git a/front/php/templates/language/es_es.json b/front/php/templates/language/es_es.json
old mode 100644
new mode 100755
diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json
old mode 100644
new mode 100755
diff --git a/front/plugins/avahi_scan/README.md b/front/plugins/avahi_scan/README.md
new file mode 100755
index 00000000..d20121e1
--- /dev/null
+++ b/front/plugins/avahi_scan/README.md
@@ -0,0 +1,7 @@
+## Overview
+
+Plugin for device name discovery via the [nbtscan](https://linuxcommandlibrary.com/man/nbtscan) network utility supporting NetBIOS.
+
+### Usage
+
+- Check the Settings page for details.
diff --git a/front/plugins/avahi_scan/avahi_scan.py b/front/plugins/avahi_scan/avahi_scan.py
new file mode 100755
index 00000000..d7bb5c55
--- /dev/null
+++ b/front/plugins/avahi_scan/avahi_scan.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+
+import os
+import pathlib
+import sys
+import json
+import sqlite3
+import subprocess
+
+# Define the installation path and extend the system path for plugin imports
+INSTALL_PATH = "/app"
+sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
+
+from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
+from plugin_utils import get_plugins_configs
+from logger import mylog
+from const import pluginsPath, fullDbPath
+from helper import timeNowTZ, get_setting_value
+from notification import write_notification
+from database import DB
+from device import Device_obj
+import conf
+from pytz import timezone
+
+# Make sure the TIMEZONE for logging is correct
+conf.tz = timezone(get_setting_value('TIMEZONE'))
+
+# Define the current path and log file paths
+CUR_PATH = str(pathlib.Path(__file__).parent.resolve())
+LOG_FILE = os.path.join(CUR_PATH, 'script.log')
+RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log')
+
+# Initialize the Plugin obj output file
+plugin_objects = Plugin_Objects(RESULT_FILE)
+
+pluginName = 'AVAHISCAN'
+
+def main():
+ mylog('verbose', [f'[{pluginName}] In script'])
+
+ # timeout = get_setting_value('AVAHI_RUN_TIMEOUT')
+ timeout = 20
+
+ # ensure service is running
+ ensure_avahi_running()
+
+ # Create a database connection
+ db = DB() # instance of class DB
+ db.open()
+
+ # Initialize the Plugin obj output file
+ plugin_objects = Plugin_Objects(RESULT_FILE)
+
+ # Create a Device_obj instance
+ device_handler = Device_obj(db)
+
+ # Retrieve devices
+ unknown_devices = device_handler.getUnknown()
+
+ # # Mock list of devices (replace with actual device_handler.getUnknown() in production)
+ # unknown_devices = [
+ # {'dev_MAC': '00:11:22:33:44:55', 'dev_LastIP': '192.168.1.121'},
+ # {'dev_MAC': '00:11:22:33:44:56', 'dev_LastIP': '192.168.1.9'},
+ # {'dev_MAC': '00:11:22:33:44:57', 'dev_LastIP': '192.168.1.82'},
+ # ]
+
+ mylog('verbose', [f'[{pluginName}] Unknown devices count: {len(unknown_devices)}'])
+
+ for device in unknown_devices:
+ domain_name = execute_name_lookup(device['dev_LastIP'], timeout)
+
+ if domain_name != '':
+ plugin_objects.add_object(
+ # "MAC", "IP", "Server", "Name"
+ primaryId = device['dev_MAC'],
+ secondaryId = device['dev_LastIP'],
+ watched1 = '', # You can add any relevant info here if needed
+ watched2 = domain_name,
+ watched3 = '',
+ watched4 = '',
+ extra = '',
+ foreignKey = device['dev_MAC'])
+
+ plugin_objects.write_result_file()
+
+ mylog('verbose', [f'[{pluginName}] Script finished'])
+
+ return 0
+
+#===============================================================================
+# Execute scan
+#===============================================================================
+def execute_name_lookup(ip, timeout):
+ """
+ Execute the avahi-resolve command on the IP.
+ """
+
+ args = ['avahi-resolve', '-a', ip]
+
+ # Execute command
+ output = ""
+
+ try:
+ mylog('verbose', [f'[{pluginName}] DEBUG CMD :', args])
+
+ # Run the subprocess with a forced timeout
+ output = subprocess.check_output(args, universal_newlines=True, stderr=subprocess.STDOUT, timeout=timeout)
+
+ mylog('verbose', [f'[{pluginName}] DEBUG OUTPUT : {output}'])
+
+ domain_name = ''
+
+ # Split the output into lines
+ lines = output.splitlines()
+
+ # Look for the resolved IP address
+ for line in lines:
+ if ip in line:
+ parts = line.split()
+ if len(parts) > 1:
+ domain_name = parts[1] # Second part is the resolved domain name
+ else:
+ mylog('verbose', [f'[{pluginName}] ⚠ ERROR - Unexpected output format: {line}'])
+
+ mylog('verbose', [f'[{pluginName}] Domain Name: {domain_name}'])
+
+ return domain_name
+
+ except subprocess.CalledProcessError as e:
+ mylog('verbose', [f'[{pluginName}] ⚠ ERROR - {e.output}'])
+
+ except subprocess.TimeoutExpired:
+ mylog('verbose', [f'[{pluginName}] TIMEOUT - the process forcefully terminated as timeout reached'])
+
+ if output == "":
+ mylog('verbose', [f'[{pluginName}] Scan: FAIL - check logs'])
+ else:
+ mylog('verbose', [f'[{pluginName}] Scan: SUCCESS'])
+
+ return ''
+
+# Function to ensure Avahi and its dependencies are running
+def ensure_avahi_running():
+ """
+ Ensure that D-Bus is running and the Avahi daemon is started.
+ """
+ mylog('verbose', [f'[{pluginName}] Ensuring D-Bus and Avahi daemon are running...'])
+
+ # # Install D-Bus if not already installed
+ # try:
+ # subprocess.run(['apk', 'add', 'dbus'], check=True)
+ # except subprocess.CalledProcessError as e:
+ # mylog('verbose', [f'[{pluginName}] ⚠ ERROR - Failed to install D-Bus: {e.output}'])
+ # return
+
+ # Start the D-Bus service
+ try:
+ subprocess.run(['rc-service', 'dbus', 'start'], check=True)
+ except subprocess.CalledProcessError as e:
+ mylog('verbose', [f'[{pluginName}] ⚠ ERROR - Failed to start D-Bus: {e.output}'])
+ return
+
+ # Create OpenRC soft level if needed
+ subprocess.run(['touch', '/run/openrc/softlevel'], check=True)
+
+ # Add Avahi daemon to runlevel
+ try:
+ subprocess.run(['rc-update', 'add', 'avahi-daemon'], check=True)
+ except subprocess.CalledProcessError as e:
+ mylog('verbose', [f'[{pluginName}] ⚠ ERROR - Failed to add Avahi to runlevel: {e.output}'])
+ return
+
+ # Start the Avahi daemon
+ try:
+ subprocess.run(['rc-service', 'avahi-daemon', 'start'], check=True)
+ except subprocess.CalledProcessError as e:
+ mylog('verbose', [f'[{pluginName}] ⚠ ERROR - Failed to start Avahi daemon: {e.output}'])
+ return
+
+ # Check status
+ status_output = subprocess.run(['rc-service', 'avahi-daemon', 'status'], capture_output=True, text=True)
+ mylog('verbose', [f'[{pluginName}] Avahi Daemon Status: {status_output.stdout.strip()}'])
+
+if __name__ == '__main__':
+ main()
diff --git a/front/plugins/avahi_scan/config.json b/front/plugins/avahi_scan/config.json
new file mode 100755
index 00000000..144a48da
--- /dev/null
+++ b/front/plugins/avahi_scan/config.json
@@ -0,0 +1,330 @@
+{
+ "code_name": "avahi_scan",
+ "unique_prefix": "AVAHISCAN",
+ "plugin_type": "other",
+ "enabled": true,
+ "data_source": "script",
+ "execution_order" : "Layer_3",
+ "show_ui": true,
+ "localized": ["display_name", "description", "icon"],
+ "display_name": [
+ {
+ "language_code": "en_us",
+ "string": "AVAHISCAN (Name discovery)"
+ }
+ ],
+ "icon": [
+ {
+ "language_code": "en_us",
+ "string": ""
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "A plugin to discover device names via mDNS."
+ }
+ ],
+ "params": [
+ {
+ "name": "ips",
+ "type": "sql",
+ "value": "SELECT dev_LastIP from DEVICES order by dev_MAC",
+ "timeoutMultiplier": true
+ }
+ ],
+ "settings": [
+ {
+ "function": "RUN",
+ "events": ["run"],
+ "type": {
+ "dataType": "string",
+ "elements": [
+ { "elementType": "select", "elementOptions": [], "transformers": [] }
+ ]
+ },
+ "default_value": "before_name_updates",
+ "options": [
+ "disabled",
+ "before_name_updates",
+ "on_new_device",
+ "once",
+ "schedule",
+ "always_after_scan"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "When to run"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Cuándo ejecutar"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Wann laufen"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "When the plugin should be executed. If enabled this will execute the scan until there are no (unknown) or (name not found) devices. Setting this to on_new_device or a daily schedule is recommended.
Depends on the SCAN_SUBNETS setting."
+ }
+ ]
+ },
+ {
+ "function": "CMD",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [{ "readonly": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "python3 /app/front/plugins/avahi_scan/nbtscan.py",
+ "options": [],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Command"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Comando"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Befehl"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Command to run. This can not be changed"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Comando a ejecutar. Esto no se puede cambiar"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Befehl zum Ausführen. Dies kann nicht geändert werden"
+ }
+ ]
+ },
+ {
+ "function": "RUN_SCHD",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ { "elementType": "input", "elementOptions": [], "transformers": [] }
+ ]
+ },
+ "default_value": "*/30 * * * *",
+ "options": [],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Schedule"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Schedule"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Schedule"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Only enabled if you select schedule in the AVAHISCAN_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."
+ },
+ {
+ "language_code": "es_es",
+ "string": "Solo está habilitado si selecciona schedule en la configuración AVAHISCAN_RUN. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en crontab.guru). Por ejemplo, ingresar 0 4 * * * ejecutará el escaneo después de las 4 a.m. en el TIMEZONE código> que configuró arriba. Se ejecutará la PRÓXIMA vez que pase el tiempo."
+ },
+ {
+ "language_code": "de_de",
+ "string": "Nur aktiviert, wenn Sie schedule in der AVAHISCAN_RUN-Einstellung auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter crontab.guru). Wenn Sie beispielsweise 0 4 * * * eingeben, wird der Scan nach 4 Uhr morgens in der TIMEZONE ausgeführt. Code> den Sie oben festgelegt haben. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht."
+ }
+ ]
+ },
+ {
+ "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"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Tiempo límite de ejecución"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Zeitüberschreitung"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Maximum time per device scan in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
+ }
+ ]
+ }
+ ],
+ "database_column_definitions": [
+ {
+ "column": "Object_PrimaryID",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "device_name_mac",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "MAC"
+ },
+ {
+ "language_code": "es_es",
+ "string": "MAC"
+ }
+ ]
+ },
+ {
+ "column": "Object_SecondaryID",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "IP"
+ },
+ {
+ "language_code": "es_es",
+ "string": "IP"
+ }
+ ]
+ },
+ {
+ "column": "Watched_Value1",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Server"
+ }
+ ]
+ },
+ {
+ "column": "Watched_Value2",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Name"
+ }
+ ]
+ },
+ {
+ "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",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Changed"
+ }
+ ]
+ },
+ {
+ "column": "Status",
+ "css_classes": "col-sm-1",
+ "show": true,
+ "type": "replace",
+ "default_value": "",
+ "options": [
+ {
+ "equals": "watched-not-changed",
+ "replacement": "