diff --git a/back/pialert.py b/back/pialert.py index 71dba460..7d31bf01 100755 --- a/back/pialert.py +++ b/back/pialert.py @@ -69,7 +69,7 @@ piholeDhcpleases = '/etc/pihole/dhcp.leases' debug_force_notification = False userSubnets = [] -changedPorts = [] +changedPorts_json_struc = None time_started = datetime.datetime.now() cron_instance = Cron() log_timestamp = time_started @@ -275,7 +275,7 @@ def importConfig (): # Webhooks global REPORT_WEBHOOK, WEBHOOK_URL, WEBHOOK_PAYLOAD, WEBHOOK_REQUEST_METHOD # Apprise - global REPORT_APPRISE, APPRISE_HOST, APPRISE_URL + global REPORT_APPRISE, APPRISE_HOST, APPRISE_URL, APPRISE_PAYLOAD # NTFY global REPORT_NTFY, NTFY_HOST, NTFY_TOPIC, NTFY_USER, NTFY_PASSWORD # PUSHSAFER @@ -343,6 +343,7 @@ def importConfig (): REPORT_APPRISE = ccd('REPORT_APPRISE', False , c_d, 'Enable Apprise', 'boolean', '', 'Apprise', ['test']) APPRISE_HOST = ccd('APPRISE_HOST', '' , c_d, 'Apprise host URL', 'text', '', 'Apprise') APPRISE_URL = ccd('APPRISE_URL', '' , c_d, 'Apprise notification URL', 'text', '', 'Apprise') + APPRISE_PAYLOAD = ccd('APPRISE_PAYLOAD', 'html' , c_d, 'Payload type', 'selecttext', "['html', 'text']", 'Apprise') # NTFY REPORT_NTFY = ccd('REPORT_NTFY', False , c_d, 'Enable NTFY', 'boolean', '', 'NTFY', ['test']) @@ -452,7 +453,7 @@ last_internet_IP_scan = now_minus_24h last_API_update = now_minus_24h last_run = now_minus_24h last_cleanup = now_minus_24h -last_update_vendors = time_started - datetime.timedelta(days = 6) # update vendors 24h after first run and than once a week +last_update_vendors = time_started - datetime.timedelta(days = 6) # update vendors 24h after first run and then once a week # indicates, if a new version is available newVersionAvailable = False @@ -1625,7 +1626,9 @@ def update_devices_names (): #------------------------------------------------------------------------------- def performNmapScan(devicesToScan): - global changedPorts + global changedPorts_json_struc + + changedPortsTmp = [] if len(devicesToScan) > 0: @@ -1735,41 +1738,39 @@ def performNmapScan(devicesToScan): indexesToDelete = indexesToDelete + str(oldEntry.index) + ',' foundEntry = oldEntry - + + columnNames = ["Name", "MAC", "Port", "State", "Service", "Extra", "NewOrOld" ] if foundEntry is not None: - changedPorts.append( - { - 'new' : { - "Name" : foundEntry.name, - "MAC" : newEntry.mac, - "Port" : newEntry.port, - "State" : newEntry.state, - "Service": newEntry.service, - "Extra" : foundEntry.extra - }, - 'old' : { - "Name" : foundEntry.name, - "MAC" : foundEntry.mac, - "Port" : foundEntry.port, - "State" : foundEntry.state, - "Service": foundEntry.service, - "Extra" : foundEntry.extra - } - } - ) + changedPortsTmp.append({ + "Name" : foundEntry.name, + "MAC" : newEntry.mac, + "Port" : newEntry.port, + "State" : newEntry.state, + "Service" : newEntry.service, + "Extra" : foundEntry.extra, + "NewOrOld" : "New values" + }) + changedPortsTmp.append({ + "Name" : foundEntry.name, + "MAC" : foundEntry.mac, + "Port" : foundEntry.port, + "State" : foundEntry.state, + "Service" : foundEntry.service, + "Extra" : foundEntry.extra, + "NewOrOld" : "Old values" + }) else: - changedPorts.append( - { - 'new' : { - "Name" : "New device", - "MAC" : newEntry.mac, - "Port" : newEntry.port, - "State" : newEntry.state, - "Service": newEntry.service, - "Extra" : "" - } - } - ) + changedPortsTmp.append({ + "Name" : "New device", + "MAC" : newEntry.mac, + "Port" : newEntry.port, + "State" : newEntry.state, + "Service" : newEntry.service, + "Extra" : "", + "NewOrOld" : "New device" + }) + + changedPorts_json_struc = json_struc({ "data" : changedPortsTmp}, columnNames) # Delete old entries if available if len(indexesToDelete) > 0: @@ -2135,14 +2136,9 @@ def skip_repeated_notifications (): json_final = [] def send_notifications (): - global mail_text, mail_html, json_final, changedPorts + global mail_text, mail_html, json_final, changedPorts_json_struc, partial_html, partial_txt, partial_json deviceUrl = REPORT_DASHBOARD_URL + '/deviceDetails.php?mac=' - table_attributes = {"style" : "border-collapse: collapse; font-size: 12px; color:#70707", "width" : "100%", "cellspacing" : 0, "cellpadding" : "3px", "bordercolor" : "#C0C0C0", "border":"1"} - headerProps = "width='120px' style='color:blue; font-size: 12px;' bgcolor='#909090' " - thProps = "width='120px' style='color:#F0F0F0' bgcolor='#909090' " - - build_direction = "TOP_TO_BOTTOM" # Reporting section file_print(' Check if something to report') @@ -2154,7 +2150,6 @@ def send_notifications (): json_events = [] json_ports = [] - # Disable reporting on events for devices where reporting is disabled based on the MAC address sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0 WHERE eve_PendingAlertEmail = 1 AND eve_EventType != 'Device Down' AND eve_MAC IN @@ -2189,161 +2184,75 @@ def send_notifications (): mail_html = mail_html.replace ('', socket.gethostname() ) if 'internet' in INCLUDED_SECTIONS: - # Compose Internet Section - text = "" - - json_string = get_table_as_json("""SELECT eve_MAC as MAC, eve_IP as IP, eve_DateTime as Datetime, eve_EventType as "Event Type", eve_AdditionalInfo as "Additional info" FROM Events + # Compose Internet Section + sqlQuery = """SELECT eve_MAC as MAC, eve_IP as IP, eve_DateTime as Datetime, eve_EventType as "Event Type", eve_AdditionalInfo as "More info" FROM Events WHERE eve_PendingAlertEmail = 1 AND eve_MAC = 'Internet' - ORDER BY eve_DateTime""") + ORDER BY eve_DateTime""" - if json_string["data"] == []: - html = "" - else: - html = convert(json_string, build_direction=build_direction, table_attributes=table_attributes) + notiStruc = construct_notifications(sqlQuery, "Internet IP change") - html = format_table(html, "data", headerProps, "Internet IP change") - - headers = ["MAC", "Datetime", "IP", "Event Type", "Additional info"] - - # prepare text-only message - text_line = '{}\t{}\n' - - for device in json_string["data"]: - for header in headers: - text += text_line.format ( header + ': ', device[header]) - - # Format HTML table headers - for header in headers: - html = format_table(html, header, thProps) - - mail_text = mail_text.replace ('', text + '\n') - mail_html = mail_html.replace ('', html) - # collect "internet" (IP changes) for the webhook json - json_internet = json_string["data"] + json_internet = notiStruc.json["data"] + + mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_html = mail_html.replace ('', notiStruc.html) if 'new_devices' in INCLUDED_SECTIONS: # Compose New Devices Section - text = "" - - json_string = get_table_as_json("""SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices + sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'New Device' - ORDER BY eve_DateTime""") - if json_string["data"] == []: - html = "" - else: - html = convert(json_string, build_direction=build_direction, table_attributes=table_attributes) + ORDER BY eve_DateTime""" - html = format_table(html, "data", headerProps, "New devices") + notiStruc = construct_notifications(sqlQuery, "New devices") - headers = ["MAC", "Datetime", "IP", "Event Type", "Device name", "Comments"] - - # prepare text-only message - text_line = '{}\t{}\n' - text = "" - for device in json_string["data"]: - for header in headers: - text += text_line.format ( header + ': ', device[header]) - - # Format HTML table headers - for header in headers: - html = format_table(html, header, thProps) - - mail_text = mail_text.replace ('', text + '\n') - mail_html = mail_html.replace ('', html) - - # collect "new_devices" for the webhook json - json_new_devices = json_string["data"] + # collect "new_devices" for the webhook json + json_new_devices = notiStruc.json["data"] + mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_html = mail_html.replace ('', notiStruc.html) if 'down_devices' in INCLUDED_SECTIONS: # Compose Devices Down Section - text = "" - - json_string = get_table_as_json("""SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices + sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'Device Down' - ORDER BY eve_DateTime""") - if json_string["data"] == []: - html = "" - else: - html = convert(json_string, build_direction=build_direction, table_attributes=table_attributes) + ORDER BY eve_DateTime""" - html = format_table(html, "data", headerProps, "Down devices") + notiStruc = construct_notifications(sqlQuery, "Down devices") - headers = ["MAC", "Datetime", "IP", "Event Type", "Device name", "Comments"] + # collect "new_devices" for the webhook json + json_down_devices = notiStruc.json["data"] - # prepare text-only message - text_line = '{}\t{}\n' - text = "" - for device in json_string["data"]: - for header in headers: - text += text_line.format ( header + ': ', device[header]) - - # Format HTML table headers - for header in headers: - html = format_table(html, header, thProps) - - mail_text = mail_text.replace ('', text + '\n') - mail_html = mail_html.replace ('', html) - - # collect "down_devices" for the webhook json - json_down_devices = json_string["data"] + mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_html = mail_html.replace ('', notiStruc.html) if 'events' in INCLUDED_SECTIONS: - # Compose Events Section - text = "" - - json_string = get_table_as_json("""SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices + # Compose Events Section + sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType IN ('Connected','Disconnected', 'IP Changed') - ORDER BY eve_DateTime""") - if json_string["data"] == []: - html = "" - else: - html = convert(json_string, build_direction=build_direction, table_attributes=table_attributes) + ORDER BY eve_DateTime""" - html = format_table(html, "data", headerProps, "Events") + notiStruc = construct_notifications(sqlQuery, "Events") - headers = ["MAC", "Datetime", "IP", "Event Type", "Device name", "Comments"] + # collect "events" for the webhook json + json_events = notiStruc.json["data"] - # prepare text-only message - text_line = '{}\t{}\n' - text = "" - for device in json_string["data"]: - for header in headers: - text += text_line.format ( header + ': ', device[header]) - - # Format HTML table headers - for header in headers: - html = format_table(html, header, thProps) - - mail_text = mail_text.replace ('', text + '\n') - mail_html = mail_html.replace ('', html) - - # collect "events" for the webhook json - json_events = json_string["data"] + mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_html = mail_html.replace ('', notiStruc.html) - if 'ports' in INCLUDED_SECTIONS: - json_ports = changedPorts + if 'ports' in INCLUDED_SECTIONS: + # collect "ports" for the webhook json + if changedPorts_json_struc is not None: + json_ports = changedPorts_json_struc.json["data"] - json_string = { "data" : changedPorts } - - if json_string["data"] == []: - html = "" - else: - html = convert(json_string, build_direction=build_direction, table_attributes=table_attributes) + notiStruc = construct_notifications("", "Ports", True, changedPorts_json_struc) - html = format_table(html, "data", headerProps, "Changed or new ports") - - headers = ["Name", "MAC", "Port", "State", "Service", "Extra"] - - for header in headers: - html = format_table(html, header, thProps) - - mail_html = mail_html.replace ('', html) + mail_html = mail_html.replace ('', notiStruc.html) + # mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_text = mail_text.replace ('', "Ports changed! Check PiAlert for details!" + '\n') json_final = { "internet": json_internet, @@ -2376,7 +2285,7 @@ def send_notifications (): if REPORT_APPRISE and check_config('apprise'): updateState("Send: Apprise") file_print(' Sending report by Apprise') - send_apprise (mail_html) + send_apprise (mail_html, mail_text) else : file_print(' Skip Apprise') if REPORT_WEBHOOK and check_config('webhook'): @@ -2415,7 +2324,7 @@ def send_notifications (): sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0 WHERE eve_PendingAlertEmail = 1""") - changedPorts = [] + changedPorts = None # DEBUG - print number of rows updated file_print(' Notifications: ', sql.rowcount) @@ -2423,6 +2332,59 @@ def send_notifications (): # Commit changes commitDB() +#------------------------------------------------------------------------------- +def construct_notifications(sqlQuery, tableTitle, skipText = False, suppliedJsonStruct = None): + + if suppliedJsonStruct is None and sqlQuery == "": + return noti_struc("", "", "") + + table_attributes = {"style" : "border-collapse: collapse; font-size: 12px; color:#70707", "width" : "100%", "cellspacing" : 0, "cellpadding" : "3px", "bordercolor" : "#C0C0C0", "border":"1"} + headerProps = "width='120px' style='color:blue; font-size: 16px;' bgcolor='#909090' " + thProps = "width='120px' style='color:#F0F0F0' bgcolor='#909090' " + + build_direction = "TOP_TO_BOTTOM" + text_line = '{}\t{}\n' + + if suppliedJsonStruct is None: + json_struc = get_table_as_json(sqlQuery) + else: + json_struc = suppliedJsonStruct + + json = json_struc.json + html = "" + text = "" + + if json["data"] != []: + + html = convert(json, build_direction=build_direction, table_attributes=table_attributes) + + html = format_table(html, "data", headerProps, tableTitle) + + headers = json_struc.columnNames + + # prepare text-only message + if skipText == False: + for device in json["data"]: + for header in headers: + padding = "" + if len(header) < 4: + padding = "\t" + text += text_line.format ( header + ': ' + padding, device[header]) + text += '\n' + + # Format HTML table headers + for header in headers: + html = format_table(html, header, thProps) + + return noti_struc(json, text, html) + +#------------------------------------------------------------------------------- +class noti_struc: + def __init__(self, json, text, html): + self.json = json + self.text = text + self.html = html + #------------------------------------------------------------------------------- def check_config(service): @@ -2680,17 +2642,22 @@ def send_webhook (_json, _html): file_print(e.output) #------------------------------------------------------------------------------- -def send_apprise (html): +def send_apprise (html, text): #Define Apprise compatible payload (https://github.com/caronc/apprise-api#stateless-solution) + payload = html + + if APPRISE_PAYLOAD == 'text': + payload = text + _json_payload={ "urls": APPRISE_URL, - "title": "Pi.Alert Notifications", - "format": "html", - "body": html + "title": "Pi.Alert Notifications", + "format": APPRISE_PAYLOAD, + "body": payload } try: - # try runnning a subprocess + # try runnning a subprocess p = subprocess.Popen(["curl","-i","-X", "POST" ,"-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), APPRISE_HOST], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, stderr = p.communicate() # write stdout and stderr into .log files for debugging if needed @@ -3139,7 +3106,7 @@ def update_api(isNotification = False): # Save selected database tables for dsSQL in dataSourcesSQLs: - json_string = get_table_as_json(dsSQL[1]) + json_string = get_table_as_json(dsSQL[1]).json write_file(folder + 'table_' + dsSQL[0] + '.json' , json.dumps(json_string)) @@ -3158,7 +3125,13 @@ def get_table_as_json(sqlQuery): tmp = fill_row(columnNames, row) result["data"].append(tmp) - return result + return json_struc(result, columnNames) + +#------------------------------------------------------------------------------- +class json_struc: + def __init__(self, json, columnNames): + self.json = json + self.columnNames = columnNames #------------------------------------------------------------------------------- def fill_row(names, row): @@ -3380,7 +3353,7 @@ def handle_test(testType): if testType == 'REPORT_WEBHOOK': send_webhook (sample_json_payload, sample_txt) if testType == 'REPORT_APPRISE': - send_apprise (sample_html) + send_apprise (sample_html, sample_txt) if testType == 'REPORT_NTFY': send_ntfy (sample_txt) if testType == 'REPORT_PUSHSAFER': @@ -3461,8 +3434,6 @@ class serviceSchedule: return result - # print("Hello my name is " + self.scheduleObject) - #=============================================================================== # BEGIN #=============================================================================== diff --git a/back/report_template.html b/back/report_template.html index 778bab3a..87beb6d8 100755 --- a/back/report_template.html +++ b/back/report_template.html @@ -32,74 +32,16 @@ - -

Internet:

+ + - - - - - - - - - -
Event Type Datetime IP Additional Info
- -
-
- - -

New Devices:

- - - - - - - - - - - -
MAC Datetime IP Device Name Vendor
- -
-
- - -

Devices Down:

- - - - - - - - - - -
MAC Datetime IP Device Name
- -
-
- - -

Events:

- - - - - - - - - - - - -
MAC Datetime IP Event Type Device Name Additional Info
-
+ + + + + + + diff --git a/back/report_template.txt b/back/report_template.txt index 9d161ec0..91f94052 100755 --- a/back/report_template.txt +++ b/back/report_template.txt @@ -13,3 +13,6 @@ Events Internet ---------------------- +Ports +---------------------- + diff --git a/docker-compose.yml b/docker-compose.yml index e60008d8..2627b339 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,7 @@ services: # (optional) useful for debugging if you have issues setting up the container - ${LOGS_LOCATION}:/home/pi/pialert/front/log # DELETE START anyone trying to use this file: comment out / delete BELOW lines, they are only for development purposes - - ${DEV_LOCATION}/back/pialert.py:/home/pi/pialert/back/pialert.py - - ${DEV_LOCATION}/back/update_vendors.sh:/home/pi/pialert/back/update_vendors.sh - - ${DEV_LOCATION}/back/report_template_new_version.html:/home/pi/pialert/back/report_template_new_version.html + - ${DEV_LOCATION}/back:/home/pi/pialert/back - ${DEV_LOCATION}/pholus:/home/pi/pialert/pholus - ${DEV_LOCATION}/dockerfiles:/home/pi/pialert/dockerfiles - ${APP_DATA_LOCATION}/pialert/php.ini:/etc/php/7.4/fpm/php.ini diff --git a/front/php/server/init.php b/front/php/server/init.php old mode 100644 new mode 100755 diff --git a/front/php/templates/language/en_us.php b/front/php/templates/language/en_us.php index 30deb2af..c3de32ba 100755 --- a/front/php/templates/language/en_us.php +++ b/front/php/templates/language/en_us.php @@ -175,7 +175,7 @@ $lang['en_us'] = array( 'DevDetail_MainInfo_Owner' => 'Owner', 'DevDetail_MainInfo_Type' => 'Type', 'DevDetail_Icon' => 'Icon', -'DevDetail_Icon_Descr' => 'Enter a font awesome icon name without the fa- prefix.', +'DevDetail_Icon_Descr' => 'Enter a font awesome icon name without the fa- prefix or with complete class, e.g.: fa fa-brands fa-apple.', 'DevDetail_MainInfo_Vendor' => 'Vendor', 'DevDetail_MainInfo_Favorite' => 'Favorite', 'DevDetail_MainInfo_Group' => 'Group', @@ -513,8 +513,8 @@ the arp-scan will take hours to complete instead of seconds. 'REPORT_DASHBOARD_URL_description' => 'This URL is used as the base for generating links in the emails. Enter full URL starting with http:// including the port number (no trailig slash /).', 'DIG_GET_IP_ARG_name' => 'Internet IP discovery', 'DIG_GET_IP_ARG_description' => 'Change the dig utility arguments if you have issues resolving your Internet IP. Arguments are added at the end of the following command: dig +short .', -'UI_LANG_name' => 'Select Language', -'UI_LANG_description' => '', +'UI_LANG_name' => 'UI Language', +'UI_LANG_description' => 'Select the preferred UI language.', //Email 'Email_settings_group' => ' Email', @@ -557,7 +557,7 @@ the arp-scan will take hours to complete instead of seconds. 'APPRISE_HOST_name' => 'Apprise host URL', 'APPRISE_HOST_description' => 'Apprise host URL starting with http:// or https://. (don\'t forget to include /notify at the end)', 'APPRISE_URL_name' => 'Apprise notification URL', -'APPRISE_URL_description' => 'Apprise notification target URL.', +'APPRISE_URL_description' => 'Apprise notification target URL. For example for Telegram it would be tgram://{bot_token}/{chat_id}.', // NTFY 'NTFY_settings_group' => ' NTFY', @@ -578,6 +578,8 @@ the arp-scan will take hours to complete instead of seconds. 'REPORT_PUSHSAFER_description' => 'Enable sending notifications via Pushsafer.', 'PUSHSAFER_TOKEN_name' => 'Pushsafer token', 'PUSHSAFER_TOKEN_description' => 'Your secret Pushsafer API key (token).', +'APPRISE_PAYLOAD_name' => 'Payload type', +'APPRISE_PAYLOAD_description' => 'Select the payoad type sent to Apprise. For example html works well with emails, text with chat apps, such as Telegram.', // MQTT diff --git a/front/settings.php b/front/settings.php index 565b9e9c..ab8e586c 100755 --- a/front/settings.php +++ b/front/settings.php @@ -279,7 +279,7 @@ while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {