diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2cd20c62..61f29310 100755 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ github: jokob-sk -patreon: 84385063 +patreon: netalertx buy_me_a_coffee: jokobsk diff --git a/Dockerfile b/Dockerfile index a19ef51e..913b64e4 100755 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ENV PATH="/opt/venv/bin:$PATH" COPY . ${INSTALL_DIR}/ -RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \ +RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \ && bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \ && bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \ && bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;" diff --git a/Dockerfile.debian b/Dockerfile.debian index b3cf222f..e43eb1c2 100755 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -43,7 +43,7 @@ RUN phpenmod -v 8.2 sqlite3 RUN apt-get install -y python3-venv RUN python3 -m venv myenv -RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag " +RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag " # Create a buildtimestamp.txt to later check if a new version was released RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt diff --git a/docs/DEBUG_PHP.md b/docs/DEBUG_PHP.md new file mode 100755 index 00000000..ab32d5a0 --- /dev/null +++ b/docs/DEBUG_PHP.md @@ -0,0 +1,34 @@ +# Debugging backend PHP issues + +## Logs in UI + +![Logs UI](./img/DEBUG/maintenance_debug_php.png) + +You can view recent backend PHP errors directly in the **Maintenance > Logs** section of the UI. This provides quick access to logs without needing terminal access. + +## Accessing logs directly + +Sometimes, the UI might not be accessible. In that case, you can access the logs directly inside the container. + +### Step-by-step: + +1. **Open a shell into the container:** + + ```bash + docker exec -it netalertx /bin/sh + ``` + +2. **Check the NGINX error log:** + + ```bash + cat /var/log/nginx/error.log + ``` + +3. **Check the PHP application error log:** + + ```bash + cat /app/log/app.php_errors.log + ``` + +These logs will help identify syntax issues, fatal errors, or startup problems when the UI fails to load properly. + diff --git a/docs/img/DEBUG/maintenance_debug_php.png b/docs/img/DEBUG/maintenance_debug_php.png new file mode 100755 index 00000000..3b59d56a Binary files /dev/null and b/docs/img/DEBUG/maintenance_debug_php.png differ diff --git a/docs/img/DEVICE_MANAGEMENT/Devices_CreateDummyDevice.png b/docs/img/DEVICE_MANAGEMENT/Devices_CreateDummyDevice.png index 009a6770..316e79c8 100755 Binary files a/docs/img/DEVICE_MANAGEMENT/Devices_CreateDummyDevice.png and b/docs/img/DEVICE_MANAGEMENT/Devices_CreateDummyDevice.png differ diff --git a/front/plugins/unifi_api_import/README.md b/front/plugins/unifi_api_import/README.md new file mode 100755 index 00000000..900fae41 --- /dev/null +++ b/front/plugins/unifi_api_import/README.md @@ -0,0 +1,26 @@ +## Overview + +Unifi import plugin using the Site Manager API. + +> [!TIP] +> The Site Manager API doesn't seems to have feature parity with the old API yet, so certain limitations apply. + +### Quick setup guide + +Navigate to your UniFi Site Manager _⚙️ Settings -> Control Plane -> Integrations_. + +- `api_key` : You can generate your API key under the _Your API Keys_ section. +- `base_url` : You can find your base url in the _API Request Format_ section, e.g. `https://192.168.100.1/proxy/network/integration/` +- `version` : You can find your version as part of the url in the _API Request Format_ section, e.g. `v1` +- `skip_ssl` : To skip SSL with you don't have an SSL certificate + + +### Usage + +- Head to **Settings** > **Plugin name** to adjust the default values. + +### Notes + +- Version: 1.0.0 +- Author: `jokob-sk` +- Release Date: `Aug 2025` \ No newline at end of file diff --git a/front/plugins/unifi_api_import/config.json b/front/plugins/unifi_api_import/config.json new file mode 100755 index 00000000..e9dac882 --- /dev/null +++ b/front/plugins/unifi_api_import/config.json @@ -0,0 +1,585 @@ +{ + "code_name": "unifi_api_import", + "unique_prefix": "UNIFIAPI", + "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": "Display Name" + } + ], + "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", + "once", + "schedule" + ], + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "When to run" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "When the plugin should run. Good options are always_after_scan, on_new_device, on_notification" + } + ] + }, + { + "function": "RUN_SCHD", + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "span", + "elementOptions": [ + { + "cssClasses": "input-group-addon validityCheck" + }, + { + "getStringKey": "Gen_ValidIcon" + } + ], + "transformers": [] + }, + { + "elementType": "input", + "elementOptions": [ + { + "onChange": "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/unifi_api_import/unifi_api_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": 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": "devCustomProps", + "type": { + "dataType": "array", + "elements": [ + { + "elementType": "popupform", + "elementHasInputValue": 1, + "elementOptions": [ + { + "fields": [ + { + "function": "hide.site.name", + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { "placeholder": "Enter value" }, + { "suffix": "_in" }, + { "cssClasses": "col-sm-10" }, + { "prefillValue": "null" } + ], + "transformers": [] + } + ] + }, + "default_value": "default", + "options": [], + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "Site name" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "The name of your site. Not used in code and only for.... " + } + ] + + }, + { + "settingKey": "hide.site.name", + "typeOverride": { + "dataType": "string", + "elements": [ + { + "elementType": "span", + "elementOptions": [ + { + "cssClasses": "input-group-addon iconPreview" + }, + { + "getStringKey": "Gen_SelectIcon" + }, + { + "customId": "CUSTPROP_icon_preview" + } + ], + "transformers": [] + }, + { + "elementType": "select", + "elementHasInputValue": 1, + "elementOptions": [ + { + "cssClasses": "iconInputVal myhidden" + }, + { + "onChange": "updateIconPreview(this)" + }, + { + "customParams": "CUSTPROP_icon,CUSTPROP_icon_preview" + } + ], + "transformers": [] + } + ] + } + }, + { + "settingKey": "hide.site.base_url", + "optionsOverride": "setting.CUSTPROP_type", + "typeOverride": { + "dataType": "string", + "elements": [ + { + "elementType": "select", + "elementOptions": [], + "transformers": [] + } + ] + } + }, + { + "settingKey": "hide.site.version" + }, + { + "settingKey": "hide.site.api_key" + }, + { + "settingKey": "hide.site.verify_ssl" + } + ] + } + ], + "transformers": [] + } + ] + }, + "default_value": [], + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Sites" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Unifi sites" + } + ] + }, + { + "function": "sites", + "type": { + "dataType": "array", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { "placeholder": "Enter value" }, + { "suffix": "_in" }, + { "cssClasses": "col-sm-10" }, + { "prefillValue": "null" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": ["_in"] }, + { "separator": "" }, + { "cssClasses": "col-xs-12" }, + { "onClick": "addList(this,false)" }, + { "getStringKey": "Gen_Add" } + ], + "transformers": [] + }, + { + "elementType": "select", + "elementHasInputValue": 1, + "elementOptions": [ + { "multiple": "true" }, + { "readonly": "true" }, + { "editable": "true" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": [] }, + { "separator": "" }, + { "cssClasses": "col-xs-6" }, + { "onClick": "removeAllOptions(this)" }, + { "getStringKey": "Gen_Remove_All" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": [] }, + { "separator": "" }, + { "cssClasses": "col-xs-6" }, + { "onClick": "removeFromList(this)" }, + { "getStringKey": "Gen_Remove_Last" } + ], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": [ + "none", + "data", + "link", + "link_new_tab", + "show_notes", + "delete_dev", + "run_plugin" + ], + "options": [], + "localized": ["name","description"], + "name": [ + { + "language_code": "en_us", + "string": "Type" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "List of property types. The default ones have specific functionality associated with it." + } + ] + } + ], + "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_Vendor", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [ + { + "language_code": "en_us", + "string": "Vendor" + } + ] + }, + { + "column": "Watched_Value3", + "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_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": "Scan method" + } + ] + }, + { + "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/front/plugins/unifi_api_import/unifi_api_import.py b/front/plugins/unifi_api_import/unifi_api_import.py new file mode 100755 index 00000000..49f2a760 --- /dev/null +++ b/front/plugins/unifi_api_import/unifi_api_import.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python + +import os +import pathlib +import sys +import json +import sqlite3 +from pytz import timezone + +# 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, Logger +from const import pluginsPath, fullDbPath, logPath +from helper import timeNowTZ, get_setting_value + +from messaging.in_app import write_notification +import conf + +# 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')) + +pluginName = '' + +# Define the current path and log file 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') + +# Initialize the Plugin obj output file +plugin_objects = Plugin_Objects(RESULT_FILE) + + + +def main(): + mylog('verbose', [f'[{pluginName}] In script']) + + # Retrieve configuration settings + some_setting = get_setting_value('SYNC_plugins') + + mylog('verbose', [f'[{pluginName}] some_setting value {some_setting}']) + + # retrieve data + device_data = get_device_data(some_setting) + + # Process the data into native application tables + if len(device_data) > 0: + + # insert devices into the lats_result.log + # make sure the below mapping is mapped in config.json, for example: + #"database_column_definitions": [ + # { + # "column": "Object_PrimaryID", <--------- the value I save into primaryId + # "mapped_to_column": "cur_MAC", <--------- gets inserted into the CurrentScan DB table column cur_MAC + # + for device in device_data: + plugin_objects.add_object( + primaryId = device['mac_address'], + secondaryId = device['ip_address'], + watched1 = device['hostname'], + watched2 = device['vendor'], + watched3 = device['device_type'], + watched4 = device['last_seen'], + extra = '', + foreignKey = device['mac_address'] + # helpVal1 = "Something1", # Optional Helper values to be passed for mapping into the app + # helpVal2 = "Something1", # If you need to use even only 1, add the remaining ones too + # helpVal3 = "Something1", # and set them to 'null'. Check the the docs for details: + # helpVal4 = "Something1", # https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS_DEV.md + ) + + mylog('verbose', [f'[{pluginName}] New entries: "{len(device_data)}"']) + + # log result + plugin_objects.write_result_file() + + return 0 + +# retrieve data +def get_device_data(some_setting): + + device_data = [] + + # do some processing, call exteranl APIs, and return a device_data list + # ... + # + # Sample data for testing purposes, you can adjust the processing in main() as needed + # ... before adding it to the plugin_objects.add_object(...) + device_data = [ + { + 'device_id': 'device1', + 'mac_address': '00:11:22:33:44:55', + 'ip_address': '192.168.1.2', + 'hostname': 'iPhone 12', + 'vendor': 'Apple Inc.', + 'device_type': 'Smartphone', + 'last_seen': '2024-06-27 10:00:00', + 'port': '1', + 'network_id': 'network1' + }, + { + 'device_id': 'device2', + 'mac_address': '00:11:22:33:44:66', + 'ip_address': '192.168.1.3', + 'hostname': 'Moto G82', + 'vendor': 'Motorola Inc.', + 'device_type': 'Laptop', + 'last_seen': '2024-06-27 10:05:00', + 'port': '', + 'network_id': 'network1' + } + ] + + # Return the data to be detected by the main application + return device_data + +if __name__ == '__main__': + main() diff --git a/install/install_dependencies.debian.sh b/install/install_dependencies.debian.sh index 81acf967..947a96ec 100755 --- a/install/install_dependencies.debian.sh +++ b/install/install_dependencies.debian.sh @@ -30,5 +30,5 @@ source myenv/bin/activate update-alternatives --install /usr/bin/python python /usr/bin/python3 10 # install packages thru pip3 -pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git +pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git diff --git a/mkdocs.yml b/mkdocs.yml index 4eb17348..5bd10d90 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - Debugging Tips: DEBUG_TIPS.md - Debugging GraphQL: DEBUG_GRAPHQL.md - Debugging Invalid JSON: DEBUG_INVALID_JSON.md + - Debugging PHP: DEBUG_PHP.md - Debugging Plugins: DEBUG_PLUGINS.md - Debugging Web UI Port: WEB_UI_PORT_DEBUG.md - Debugging Workflows: WORKFLOWS_DEBUGGING.md