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": "
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. Watched_Value1 is Hostname Watched_Value2 is Vendor Watched_Value3 is Type Watched_Value4 is Network 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.'