diff --git a/Dockerfile b/Dockerfile index 1580bc27..13fab0d2 100755 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ENV USER=pi USER_ID=1000 USER_GID=1000 TZ=Europe/London PORT=20211 RUN apt-get update \ && apt-get install --no-install-recommends tini 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 python3 iproute2 nmap python3-pip zip -y \ - && pip3 install requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases \ + && pip3 install requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi \ && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 \ && apt-get clean autoclean \ && apt-get autoremove \ diff --git a/README.md b/README.md index e78736b6..eb87d797 100755 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ The system continuously scans the network for, **New devices**, **New connection - Monitor anything for changes - Check the instructions carefully if you are up for a challenge! Current plugins include: - Detecting Rogue DHCP servers - - Monitoring HTTP status changes of domains/URLs + - Monitoring HTTP status changes of domains/URLs + - Import devices from DHCP.leases files or a UniFi controller | ![Screen 1][screen1] | ![Screen 2][screen2] | ![Screen 5][screen5] | |----------------------|----------------------| ----------------------| diff --git a/front/plugins/README.md b/front/plugins/README.md index 79bfd805..73024408 100755 --- a/front/plugins/README.md +++ b/front/plugins/README.md @@ -12,7 +12,7 @@ These issues will be hopefully fixed with time, so please don't report them. Ins ## Overview -PiAlert comes with a plugin system to feed events from third-party scripts into the UI and then send notifications, if desired. The highlighted functionality this plugin system supports, is dynamic creation of a simple UI to interact with the discovered objects, a mechanism to surface settings of plugins in the UI, or to import objects into existing PiAlert database tables. +PiAlert comes with a plugin system to feed events from third-party scripts into the UI and then send notifications, if desired. The highlighted functionality this plugin system supports, is dynamic creation of a simple UI to interact with the discovered objects, a mechanism to surface settings of plugins in the UI, or to import objects into existing PiAlert database tables. (Currently update/overwriting of existing objects is not supported.) Example use cases for plugins could be: @@ -42,8 +42,6 @@ Again, please read the below carefully if you'd like to contribute with a plugin More on specifics below. - - ### Column order and values | Order | Represented Column | Required | Description | @@ -59,7 +57,7 @@ More on specifics below. | 8 | `ForeignKey` | no | A foreign key that can be used to link to the parent object (usually a MAC address) | -### config.json +# config.json structure ## Supported data sources @@ -433,6 +431,7 @@ The UI will adjust how columns are displayed in the UI based on the definition o - [website_monitor (WEBMON) config.json](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/website_monitor/config.json) - [dhcp_servers (DHCPSRVS) config.json](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/dhcp_servers/config.json) - [dhcp_leases (DHCPLSS) config.json](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/dhcp_leases/config.json) +- [unifi_import (UNFIMP) config.json](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/unifi_import/config.json) ### SQL query based plugins - [nmap_services (NMAPSERV) config.json](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/nmap_services/config.json) diff --git a/front/plugins/unifi_import/README.md b/front/plugins/unifi_import/README.md new file mode 100755 index 00000000..34a22897 --- /dev/null +++ b/front/plugins/unifi_import/README.md @@ -0,0 +1,18 @@ +## Overview + +A plugin allowing for importing devices from an UniFi controller. + +### Usage + +Spedify the following settings in the Settings section of PiAlert: + +- `UNFIMP_username` - Username used to login into the UNIFI controller. +- `UNFIMP_password` - Password used to login into the UNIFI controller. +- `UNFIMP_host` - Host url or IP address where the UNIFI controller is hosted (excluding http://) +- `UNFIMP_sites` - Name of the sites (usually 'default', check the URL in your UniFi controller UI if unsure. The site id is in the following part of the URL: `https://192.168.1.1:8443/manage/site/this-is-the-site-id/settings/`). +- `UNFIMP_protocol` - https:// or http:// +- `UNFIMP_port` - Usually 8443 + +### Notes + +- Currently only used to import devices, not their status, type or network map. \ No newline at end of file diff --git a/front/plugins/unifi_import/config.json b/front/plugins/unifi_import/config.json new file mode 100755 index 00000000..d867909d --- /dev/null +++ b/front/plugins/unifi_import/config.json @@ -0,0 +1,428 @@ +{ + "code_name": "unifi_import", + "unique_prefix": "UNFIMP", + "enabled": true, + "data_source": "python-script", + "localized": ["display_name", "description", "icon"], + "mapped_to_table": "DHCP_Leases", + "display_name" : [{ + "language_code":"en_us", + "string" : "UniFi import" + }], + "icon":[{ + "language_code":"en_us", + "string" : "" + }], + "description": [{ + "language_code":"en_us", + "string" : "This plugin is used to import devices from an UNIFI controller." + }], + "params" : [ + { + "name" : "username", + "type" : "setting", + "value" : "UNFIMP_username" + }, + { + "name" : "password", + "type" : "setting", + "value" : "UNFIMP_password" + }, + { + "name" : "host", + "type" : "setting", + "value" : "UNFIMP_host" + }, + { + "name" : "sites", + "type" : "setting", + "value" : "UNFIMP_sites" + }, + { + "name" : "protocol", + "type" : "setting", + "value" : "UNFIMP_protocol" + }, + { + "name" : "port", + "type" : "setting", + "value" : "UNFIMP_port" + } + ], + "database_column_definitions": + [ + { + "column": "Index", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "N/A" + }] + } , + { + "column": "Plugin", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "N/A" + }] + }, + { + "column": "Object_PrimaryID", + "mapped_to_column": "DHCP_MAC", + "css_classes": "col-sm-2", + "show": true, + "type": "devicemac", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "MAC address" + }] + }, + { + "column": "Object_SecondaryID", + "mapped_to_column": "DHCP_IP", + "css_classes": "col-sm-2", + "show": true, + "type": "deviceip", + "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": "DHCP_DateTime", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Changed" + }] + }, + { + "column": "Watched_Value1", + "mapped_to_column": "DHCP_Name", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Hostname" + }] + }, + { + "column": "Watched_Value2", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Vendor" + }] + }, + { + "column": "Watched_Value3", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Type" + }] + } , + { + "column": "Watched_Value4", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Network" + }] + } , + { + "column": "UserData", + "css_classes": "col-sm-2", + "show": false, + "type": "textboxsave", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Comments" + }] + }, + { + "column": "Extra", + "css_classes": "col-sm-3", + "show": true, + "type": "label", + "default_value":"", + "options": [], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Hostname" + }] + }, + { + "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": "
" + } + ], + "localized": ["name"], + "name":[{ + "language_code":"en_us", + "string" : "Status" + }] + } + ], + "settings":[ + { + "function": "RUN", + "type": "selecttext", + "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 a UNIFI controller. 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." + }] + }, + { + "function": "CMD", + "type": "text", + "default_value":"python3 /home/pi/pialert/front/plugins/unifi_import/script.py username={username} password={password} host={host} sites={sites} protocol={protocol} port={port}", + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Command" + }], + "description": [{ + "language_code":"en_us", + "string" : "Command to run. Not recommended to change." + }] + }, + { + "function": "username", + "type": "text", + "default_value":"", + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Username" + }], + "description": [{ + "language_code":"en_us", + "string" : "The username used to login into your UNIFI controller. It is recommended to create a read-only user account." + }] + }, + { + "function": "password", + "type": "password", + "default_value":"", + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Password" + }], + "description": [{ + "language_code":"en_us", + "string" : "The password used to login into your UNIFI controller." + }] + }, + { + "function": "protocol", + "type": "selecttext", + "default_value":"https://", + "options": ["https://", "http://"], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Protocol" + }], + "description": [{ + "language_code":"en_us", + "string" : "The protocol to use to access the controller." + }] + }, + { + "function": "host", + "type": "text", + "default_value":"192.168.1.1", + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Host" + }], + "description": [{ + "language_code":"en_us", + "string" : "The host (IP) where the UNIFI controller is runnig. Do NOT include the protocol (e.g. https://)" + }] + }, + { + "function": "port", + "type": "text", + "default_value":"8443", + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Port number" + }], + "description": [{ + "language_code":"en_us", + "string" : "The port number where the UNIFI controller is runnig. Usually it is 8443." + }] + }, + { + "function": "sites", + "type": "list", + "default_value":["default"], + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "UNIFI sites" + }], + "description": [{ + "language_code":"en_us", + "string" : "The sites you want to connect to. Usually it is only one and the name is default. Check the URL in your UniFi controller UI if unsure." + }] + }, + { + "function": "RUN_SCHD", + "type": "text", + "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 DHCPLSS_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": "RUN_TIMEOUT", + "type": "integer", + "default_value":5, + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Run timeout" + }, + { + "language_code":"de_de", + "string" : "Wartezeit" + }], + "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": "WATCH", + "type": "multiselect", + "default_value":["Watched_Value1", "Watched_Value4"], + "options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"], + "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. " + }] + }, + { + "function": "REPORT_ON", + "type": "multiselect", + "default_value":["new","watched-changed"], + "options": ["new","watched-changed","watched-not-changed"], + "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 Watched_ValueN columns changed." + }] + } + ] +} + diff --git a/front/plugins/unifi_import/script.py b/front/plugins/unifi_import/script.py new file mode 100755 index 00000000..00f4bcd8 --- /dev/null +++ b/front/plugins/unifi_import/script.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# Based on the work of https://github.com/stevehoek/Pi.Alert + +# Example call +# python3 /home/pi/pialert/front/plugins/unifi_import/script.py username=pialert password=passw0rd host=192.168.1.1 site=default protocol=https:// port=8443 + +from __future__ import unicode_literals +from time import sleep, time, strftime +import requests +from requests import Request, Session, packages +import pathlib +import threading +import subprocess +import socket +import json +import argparse +import io +import sys +from requests.packages.urllib3.exceptions import InsecureRequestWarning +import pwd +import os +from unificontrol import UnifiClient +from pyunifi.controller import Controller + + +curPath = str(pathlib.Path(__file__).parent.resolve()) +log_file = curPath + '/script.log' +last_run = curPath + '/last_result.log' + +# Workflow + +def main(): + + # init global variables + global UNIFI_USERNAME, UNIFI_PASSWORD, UNIFI_HOST + global UNIFI_REQUIRE_PRIVATE_IP, UNIFI_SKIP_NAMED_GUESTS, UNIFI_SKIP_GUESTS, UNIFI_SITES, PORT, PROTOCOL + + last_run_logfile = open(last_run, 'a') + + # empty file + last_run_logfile.write("") + + parser = argparse.ArgumentParser(description='Import devices from an UNIFI controller') + + parser.add_argument('username', action="store", help="Username used to login into the UNIFI controller") + parser.add_argument('password', action="store", help="Password used to login into the UNIFI controller") + parser.add_argument('host', action="store", help="Host url or IP address where the UNIFI controller is hosted (excluding http://)") + parser.add_argument('sites', action="store", help="Name of the sites (usually 'default', check the URL in your UniFi controller UI). Separated by comma (,) if passing multiple sites") + parser.add_argument('protocol', action="store", help="https:// or http://") + parser.add_argument('port', action="store", help="Usually 8443") + + values = parser.parse_args() + + # parse output + newEntries = [] + + if values.username and values.password and values.host and values.sites: + + UNIFI_USERNAME = values.username.split('=')[1] + UNIFI_PASSWORD = values.password.split('=')[1] + UNIFI_HOST = values.host.split('=')[1] + UNIFI_SITES = values.sites.split('=')[1] + PROTOCOL = values.protocol.split('=')[1] + PORT = values.port.split('=')[1] + + newEntries = get_entries(newEntries) + + + for e in newEntries: + # Insert list into the log + service_monitoring_log(e.primaryId, e.secondaryId, e.created, e.watched1, e.watched2, e.watched3, e.watched4, e.extra, e.foreignKey ) + + + +# ----------------------------------------------------------------------------- +def get_entries(newEntries): + + sites = [] + + if ',' in UNIFI_SITES: + sites = UNIFI_SITES.split(',') + + else: + sites.append(UNIFI_SITES) + + + for site in sites: + + c = Controller(UNIFI_HOST, UNIFI_USERNAME, UNIFI_PASSWORD, ssl_verify=False, site_id=site ) + + for ap in c.get_aps(): + + # print(f'{json.dumps(ap)}') + + deviceType = '' + if (ap['type'] == 'udm'): + deviceType = 'Router' + elif (ap['type'] == 'usg'): + deviceType = 'Router' + elif (ap['type'] == 'usw'): + deviceType = 'Switch' + elif (ap['type'] == 'uap'): + deviceType = 'AP' + + name = get_unifi_val(ap, 'name') + hostName = get_unifi_val(ap, 'hostname') + + if name == 'null' and hostName != 'null': + name = hostName + + tmpPlugObj = plugin_object_class( + ap['mac'], + ap['ip'], + name, + 'Ubiquiti Networks Inc.', + deviceType, + ap['state'], + get_unifi_val(ap, 'connection_network_name') + ) + + newEntries.append(tmpPlugObj) + + # print(f'>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + + for cl in c.get_clients(): + + # print(f'{json.dumps(cl)}') + + name = get_unifi_val(cl, 'name') + hostName = get_unifi_val(cl, 'hostname') + + if name == 'null' and hostName != 'null': + name = hostName + + tmpPlugObj = plugin_object_class( + cl['mac'], + cl['ip'], + name, + get_unifi_val(cl, 'oui'), + 'Other', + 1, + get_unifi_val(cl, 'connection_network_name') + ) + + # print(f'>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + + for us in c.get_clients(): + + # print(f'{json.dumps(us)}') + + name = get_unifi_val(us, 'name') + hostName = get_unifi_val(us, 'hostname') + + if name == 'null' and hostName != 'null': + name = hostName + + tmpPlugObj = plugin_object_class( + us['mac'], + us['ip'], + name, + get_unifi_val(us, 'oui'), + 'Other', + 1, + get_unifi_val(us, 'connection_network_name') + ) + + newEntries.append(tmpPlugObj) + + return newEntries + + +# ----------------------------------------------------------------------------- +def get_unifi_val(obj, key): + + res = '' + + if key in obj: + res = obj[key] + + if res not in ['','None']: + return res + + if obj.get(key) is not None: + res = obj.get(key) + + if res not in ['','None']: + return res + + return 'null' + +# ------------------------------------------------------------------- +class plugin_object_class: + def __init__(self, primaryId = '',secondaryId = '', watched1 = '',watched2 = '',watched3 = '',watched4 = '',extra = '',foreignKey = ''): + self.pluginPref = '' + self.primaryId = primaryId + self.secondaryId = secondaryId + self.created = strftime("%Y-%m-%d %H:%M:%S") + self.changed = '' + self.watched1 = watched1 + self.watched2 = watched2 + self.watched3 = watched3 + self.watched4 = watched4 + self.status = '' + self.extra = extra + self.userData = '' + self.foreignKey = foreignKey + +# ----------------------------------------------------------------------------- +def service_monitoring_log(primaryId, secondaryId, created, watched1, watched2 = 'null', watched3 = 'null', watched4 = 'null', extra ='null', foreignKey ='null' ): + + if watched1 == '': + watched1 = 'null' + if watched2 == '': + watched2 = 'null' + if watched3 == '': + watched3 = 'null' + if watched4 == '': + watched4 = 'null' + if extra == '': + extra = 'null' + if foreignKey == '': + foreignKey = 'null' + + with open(last_run, 'a') as last_run_logfile: + # https://www.duckduckgo.com|192.168.0.1|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine|null + last_run_logfile.write("{}|{}|{}|{}|{}|{}|{}|{}|{}\n".format( + primaryId, + secondaryId, + created, + watched1, + watched2, + watched3, + watched4, + extra, + foreignKey + ) + ) + + +#=============================================================================== +# BEGIN +#=============================================================================== +if __name__ == '__main__': + main() + diff --git a/front/settings.php b/front/settings.php index 08c56104..7a44c45c 100755 --- a/front/settings.php +++ b/front/settings.php @@ -32,6 +32,7 @@ $result = $db->query("SELECT * FROM Settings"); // array $settingKeyOfLists = array(); +$settingCoreGroups = array('General', 'Email', 'Webhooks', 'Apprise', 'NTFY', 'PUSHSAFER', 'MQTT', 'DynDNS', 'PiHole', 'Pholus', 'Nmap', 'API'); $settings = array(); while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { // Push row data @@ -82,10 +83,18 @@ while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { $isIn = ' in '; foreach ($groups as $group) { + if (in_array($group, $settingCoreGroups)) + { + $settingGroupTypeHtml = ""; + } else + { + $settingGroupTypeHtml = ' () '; + } + $html = $html.'
-

'.lang($group.'_icon')." ".lang($group.'_display_name').'

+

'.lang($group.'_icon')." ".lang($group.'_display_name').$settingGroupTypeHtml.'