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 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 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": "
" + }, + { + "equals": "watched-changed", + "replacement": "
" + }, + { + "equals": "new", + "replacement": "
" + }, + { + "equals": "missing-in-last-scan", + "replacement": "
" + } + ], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "Status" + } + ] + } + ] +} diff --git a/install/install_dependencies.debian.sh b/install/install_dependencies.debian.sh index 664e9ee5..28cd57ff 100755 --- a/install/install_dependencies.debian.sh +++ b/install/install_dependencies.debian.sh @@ -16,7 +16,7 @@ fi 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 php-openssl sqlite3 dnsutils net-tools \ - python3 python3-dev iproute2 nmap python3-pip zip systemctl usbutils traceroute nbtscan build-essential + python3 python3-dev iproute2 nmap python3-pip zip systemctl usbutils traceroute nbtscan avahi avahi-tools openrc dbus build-essential # alternate dependencies sudo apt-get install nginx nginx-core mtr php-fpm php8.2-fpm php-cli php8.2 php8.2-sqlite3 -y diff --git a/server/device.py b/server/device.py index b46fbc68..f27e77da 100755 --- a/server/device.py +++ b/server/device.py @@ -4,7 +4,7 @@ import subprocess import conf import os import re -from helper import timeNowTZ, get_setting, get_setting_value, list_to_where, resolve_device_name_dig, resolve_device_name_pholus, get_device_name_nbtlookup, get_device_name_nslookup, check_IP_format, sanitize_SQL_input +from helper import timeNowTZ, get_setting, get_setting_value, list_to_where, resolve_device_name_dig, resolve_device_name_pholus, get_device_name_nbtlookup, get_device_name_nslookup, get_device_name_mdns, check_IP_format, sanitize_SQL_input from logger import mylog, print_log from const import vendorsPath, vendorsPathNewest, sql_generateGuid @@ -465,6 +465,7 @@ def update_devices_names (db): notFound = 0 foundDig = 0 + foundmDNSLookup = 0 foundNsLookup = 0 foundNbtLookup = 0 foundPholus = 0 @@ -499,6 +500,13 @@ def update_devices_names (db): # count if newName != nameNotFound: foundDig += 1 + + # Resolve device name with AVAHISCAN plugin data + if newName == nameNotFound: + newName = get_device_name_mdns(db, device['dev_MAC'], device['dev_LastIP']) + + if newName != nameNotFound: + foundmDNSLookup += 1 # Resolve device name with NSLOOKUP plugin data if newName == nameNotFound: @@ -542,8 +550,8 @@ def update_devices_names (db): recordsToUpdate.append ([newName, device['dev_MAC']]) # Print log - mylog('verbose', ['[Update Device Name] Names Found (DiG/NSLOOKUP/NBTSCAN/Pholus): ', len(recordsToUpdate), " (",foundDig,"/",foundNsLookup,"/",foundNbtLookup,"/", foundPholus ,")"] ) - mylog('verbose', ['[Update Device Name] Names Not Found : ', notFound] ) + mylog('verbose', [f'[Update Device Name] Names Found (DiG/mDNS/NSLOOKUP/NBTSCAN/Pholus): {len(recordsToUpdate)} ( {foundmDNSLookup}/{foundDig}/{foundNsLookup}/{foundNbtLookup}/{foundPholus})'] ) + mylog('verbose', [f'[Update Device Name] Names Not Found : {notFound}'] ) # update not found devices with (name not found) sql.executemany ("UPDATE Devices SET dev_Name = ? WHERE dev_MAC = ? ", recordsNotFound ) diff --git a/server/helper.py b/server/helper.py index cb6f3163..4c6010b0 100755 --- a/server/helper.py +++ b/server/helper.py @@ -510,7 +510,50 @@ def check_IP_format (pIP): # Return IP return IP.group(0) +#------------------------------------------------------------------------------- +def get_device_name_mdns(db, pMAC, pIP): + + nameNotFound = "(name not found)" + sql = db.sql + + name = nameNotFound + + # get names from the AVAHISCAN plugin entries vased on MAC + sql.execute( + f""" + SELECT Watched_Value2 FROM Plugins_Objects + WHERE + Plugin = 'AVAHISCAN' AND + Object_PrimaryID = '{pMAC}' + """ + ) + nameEntry = sql.fetchall() + db.commitDB() + + if len(nameEntry) != 0: + name = cleanDeviceName(nameEntry[0][0], False) + + return name + + # get names from the AVAHISCAN plugin entries based on IP + sql.execute( + f""" + SELECT Watched_Value2 FROM Plugins_Objects + WHERE + Plugin = 'AVAHISCAN' AND + Object_SecondaryID = '{pIP}' + """ + ) + nameEntry = sql.fetchall() + db.commitDB() + + if len(nameEntry) != 0: + name = cleanDeviceName(nameEntry[0][0], True) + + return name + + return name #------------------------------------------------------------------------------- def get_device_name_nslookup(db, pMAC, pIP):