diff --git a/front/plugins/adguard_import/README.md b/front/plugins/adguard_import/README.md new file mode 100644 index 00000000..f99a3b36 --- /dev/null +++ b/front/plugins/adguard_import/README.md @@ -0,0 +1,27 @@ +## Overview + +Plugin functionality overview and links to external resources if relevant. Include use cases if available. + +> [!TIP] +> Some tip. + +### Quick setup guide + +To set up the plugin correctly, make sure... + +#### Required Settings + +- When to run `PREF_RUN` +- + +### Usage + +- Head to **Settings** > **Plugin name** to adjust the default values. + +### Notes + +- Additional notes, limitations, Author info. + +- Version: 1.0.0 +- Author: `` +- Release Date: `` \ No newline at end of file diff --git a/front/plugins/adguard_import/adguard_import.py b/front/plugins/adguard_import/adguard_import.py new file mode 100644 index 00000000..46fc86ae --- /dev/null +++ b/front/plugins/adguard_import/adguard_import.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +import os +import sys +import requests +from pytz import timezone + +# Define the installation path and extend the system path for plugin imports +INSTALL_PATH = os.getenv('NETALERTX_APP', '/app') +sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) + +from const import logPath # noqa: E402, E261 +from plugin_helper import Plugin_Objects # noqa: E402, E261 +from utils.crypto_utils import string_to_mac_hash # noqa: E402 [flake8 lint suppression] +from logger import mylog, Logger # noqa: E402, E261 +from helper import get_setting_value # noqa: E402, E261 +import conf # noqa: E402, E261 + +# ---------------------------- +# Plugin metadata +# ---------------------------- +pluginName = "ADGUARDIMP" + +# Make sure the TIMEZONE for logging is correct +conf.tz = timezone(get_setting_value("TIMEZONE")) + +# Make sure log level is initialized correctly +Logger(get_setting_value("LOG_LEVEL")) + +# Define paths +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) + + +# ---------------------------- +# Helpers +# ---------------------------- +def ag_request(path, server, port, protocol, auth, timeout): + """Unified request handler""" + url = f"{protocol}://{server}:{port}{path}" + + try: + r = requests.get(url, auth=auth, timeout=timeout, verify=False) + if r.status_code != 200: + mylog("none", [f"[{pluginName}] Failed request {url} -> {r.status_code}"]) + return None + return r.json() + except Exception as e: + mylog("none", [f"[{pluginName}] Exception accessing {url}: {e}"]) + return None + + +# ---------------------------- +# MAIN +# ---------------------------- +def main(): + mylog("verbose", [f"[{pluginName}] In script"]) + + # Retrieve plugin settings + server = get_setting_value("ADGUARDIMP_SERVER") + port = get_setting_value("ADGUARDIMP_PORT") + protocol = get_setting_value("ADGUARDIMP_PROTOCOL") or "http" + user = get_setting_value("ADGUARDIMP_USER") + pw = get_setting_value("ADGUARDIMP_PASS") + fake_mac_enabled = get_setting_value("ADGUARDIMP_FAKE_MAC") + timeout = int(get_setting_value("ADGUARDIMP_RUN_TIMEOUT") or 5) + + auth = (user, pw) if user or pw else None + + # ------------------------------------------- + # Fetch clients from AdGuard Home + # ------------------------------------------- + clients_json = ag_request( + "/control/clients", + server, port, protocol, auth, timeout + ) + + if not clients_json: + mylog("none", [f"[{pluginName}] No clients returned"]) + plugin_objects.write_result_file() + return 1 + + raw_clients = clients_json.get("auto_clients", []) or [] + + # ------------------------------------------- + # Fetch DHCP leases if DHCP enabled + # ------------------------------------------- + dhcp_json = ag_request( + "/control/dhcp/status", + server, port, protocol, auth, timeout + ) + + dhcp_leases = [] + if dhcp_json and dhcp_json.get("enabled"): + dhcp_leases = dhcp_json.get("leases", []) + + # Build MAC lookup table for DHCP + dhcp_mac_map = {} + for lease in dhcp_leases: + ip = lease.get("ip") + mac = lease.get("mac") + if ip and mac: + dhcp_mac_map[ip] = mac.upper() + + # ------------------------------------------- + # Process devices + # ------------------------------------------- + device_data = [] + + for cl in raw_clients: + ip = cl.get("ip") + hostname = cl.get("name") or "" + dsource = cl.get("source") or "" + + # Determine MAC + mac = dhcp_mac_map.get(ip) + + if not mac and fake_mac_enabled: + mylog("verbose", [f"[{pluginName}] Generating FAKE MAC for ip: {ip}"]) + mac = string_to_mac_hash(ip) + + if not mac: + # Skip devices without MAC if fake MAC not allowed + mylog("verbose", [f"[{pluginName}] Skipping device with {ip} as no MAC supplied and ADGUARDIMP_FAKE_MAC set to False"]) + continue + + device_data.append({ + "mac_address": mac, + "ip_address": ip, + "hostname": hostname, + "device_type": dsource + }) + + # ------------------------------------------- + # Write plugin objects + # ------------------------------------------- + for dev in device_data: + plugin_objects.add_object( + primaryId = dev["mac_address"], + secondaryId = dev["ip_address"], + watched1 = dev["hostname"], + watched2 = dev["device_type"], + watched3 = '', + watched4 = '', + extra = '', + foreignKey = dev["mac_address"], + ) + + mylog("verbose", [f"[{pluginName}] New entries: {len(device_data)}"]) + plugin_objects.write_result_file() + return 0 + + +if __name__ == "__main__": + main() diff --git a/front/plugins/adguard_import/config.json b/front/plugins/adguard_import/config.json new file mode 100644 index 00000000..8a9e8530 --- /dev/null +++ b/front/plugins/adguard_import/config.json @@ -0,0 +1,504 @@ +{ + "code_name": "adguard_import", + "unique_prefix": "ADGUARDIMP", + "plugin_type": "device_scanner", + "execution_order" : "Layer_0", + "enabled": true, + "data_source": "script", + "mapped_to_table": "CurrentScan", + "data_filters": [ + { + "compare_column": "Object_PrimaryID", + "compare_operator": "==", + "compare_field_id": "txtMacFilter", + "compare_js_template": "'{value}'.toString()", + "compare_use_quotes": true + } + ], + "show_ui": true, + "localized": ["display_name", "description", "icon"], + "display_name": [ + { + "language_code": "en_us", + "string": "AdGuard (Device import)" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Plugin to ..." + } + ], + "icon": [ + { + "language_code": "en_us", + "string": "" + } + ], + "params": [], + "settings": [ + { + "function": "RUN", + "events": ["run"], + "type": { + "dataType": "string", + "elements": [ + { "elementType": "select", "elementOptions": [], "transformers": [] } + ] + }, + + "default_value": "disabled", + "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" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "When the plugin should run. A good option is schedule for device scanners." + } + ] + }, + { + "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": "*/5 * * * *", + "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 SYNC_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." + } + ] + }, + { + "function": "CMD", + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "readonly": "true" }], + "transformers": [] + } + ] + }, + "default_value": "python3 /app/front/plugins/adguard_import/adguard_import.py", + "options": [], + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "Command" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Command to run. This can not be changed" + } + ] + }, + { + "function": "RUN_TIMEOUT", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "type": "number" }], + "transformers": [] + } + ] + }, + "default_value": 30, + "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": "SERVER", + "type": { + "dataType": "string", + "elements": [ + { "elementType": "input", "elementOptions": [], "transformers": [] } + ] + }, + "maxLength": 200, + "default_value": "", + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "AdGuard Home Server" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Hostname or IP of your AdGuard Home server.
Example: 192.168.1.10 or adguard.local" + } + ] + }, + { + "function": "PORT", + "type": { + "dataType": "integer", + "elements": [ + { "elementType": "input", "elementOptions": [{ "type": "number" }], "transformers": [] } + ] + }, + "default_value": 3000, + "localized": ["name", "description"], + "name": [ + { "language_code": "en_us", "string": "Port" } + ], + "description": [ + { + "language_code": "en_us", + "string": "Port used by AdGuard Home API. Default is normally 3000." + } + ] + }, + { + "function": "PROTOCOL", + "type": { + "dataType": "string", + "elements": [ + { "elementType": "select", "elementOptions": [], "transformers": [] } + ] + }, + "default_value": "http", + "options": ["http", "https"], + "localized": ["name", "description"], + "name": [ + { "language_code": "en_us", "string": "Protocol" } + ], + "description": [ + { + "language_code": "en_us", + "string": "Choose whether to use HTTP or HTTPS to connect to the AdGuard Home API." + } + ] + }, + { + "function": "USER", + "type": { + "dataType": "string", + "elements": [ + { "elementType": "input", "elementOptions": [], "transformers": [] } + ] + }, + "maxLength": 200, + "default_value": "", + "localized": ["name", "description"], + "name": [ + { "language_code": "en_us", "string": "Username" } + ], + "description": [ + { + "language_code": "en_us", + "string": "API username for AdGuard Home. Leave empty if your API does not require login." + } + ] + }, + { + "function": "PASS", + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "type": "password" }], + "transformers": [] + } + ] + }, + "maxLength": 200, + "default_value": "", + "localized": ["name", "description"], + "name": [ + { "language_code": "en_us", "string": "Password" } + ], + "description": [ + { + "language_code": "en_us", + "string": "API password for AdGuard Home. Leave empty if authentication is disabled." + } + ] + }, + { + "function": "FAKE_MAC", + "type": { + "dataType": "boolean", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "type": "checkbox" }], + "transformers": [] + } + ] + }, + "default_value": false, + "localized": ["name", "description"], + "name": [ + { "language_code": "en_us", "string": "Generate Fake MACs" } + ], + "description": [ + { + "language_code": "en_us", + "string": "Some devices don't have a MAC assigned. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped." + } + ] + } + ], + "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": "Object_PrimaryID", + "mapped_to_column": "cur_MAC", + "css_classes": "col-sm-3", + "show": true, + "type": "device_name_mac", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "MAC (name)" + } + ] + }, + { + "column": "Object_SecondaryID", + "mapped_to_column": "cur_IP", + "css_classes": "col-sm-2", + "show": true, + "type": "device_ip", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "IP" + } + ] + }, + { + "column": "Watched_Value1", + "mapped_to_column": "cur_Name", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "Name" + } + ] + }, + { + "column": "Watched_Value2", + "mapped_to_column": "cur_Type", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "Device Type" + } + ] + }, + { + "column": "Watched_Value3", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + } + ] + }, + { + "column": "Watched_Value4", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + } + ] + }, + { + "column": "Dummy", + "mapped_to_column": "cur_ScanMethod", + "mapped_to_column_data": { + "value": "Example Plugin" + }, + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "ADGUARDIMP" + } + ] + }, + { + "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/server/initialise.py b/server/initialise.py index 764979d4..4d3f96b3 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -490,7 +490,7 @@ def importConfigs(pm, db, all_plugins): c_d, set["name"][0]["string"], set["type"], - str(set["options"]), + str(set.get("options", [])), group=pref, events=set.get("events"), desc=set["description"][0]["string"],