Merge branch 'main' into devving-devcontainer

This commit is contained in:
Adam Outler
2025-09-20 18:42:34 -04:00
committed by GitHub
7 changed files with 178 additions and 133 deletions

View File

@@ -62,7 +62,7 @@ function renderLogArea($params) {
'</textarea>
</div>
<div class="row logs-row">
<div class="log-file col-sm-6 col-xs-12">' . htmlspecialchars($fileName) . '
<div class="log-file col-sm-6 col-xs-12">' . htmlspecialchars($filePath) . '
<div class="logs-size">' . number_format((filesize($filePath) / 1000000), 2, ",", ".") . ' MB'
. $downloadButtonHtml .
'</div>

View File

@@ -82,8 +82,7 @@ class CustomDatabaseWrapper {
private $maxRetries;
private $retryDelay;
public function __construct($filename, $flags = SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE,
$maxRetries = 3, $retryDelay = 1000, $encryptionKey = "") {
public function __construct($filename, $flags = SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $maxRetries = 3, $retryDelay = 1000, $encryptionKey = "") {
$this->sqlite = new SQLite3($filename, $flags, $encryptionKey);
$this->maxRetries = $maxRetries;
$this->retryDelay = $retryDelay;

View File

@@ -48,7 +48,7 @@ if (!empty($_REQUEST['action']) && $_REQUEST['action'] == 'logout') {
// Load configuration
if (!file_exists(CONFIG_PATH)) {
die("Configuration file not found.");
die("Configuration file not found in " . $_SERVER['DOCUMENT_ROOT'] . "/../config/app.conf");
}
$configLines = file(CONFIG_PATH);

View File

@@ -14,7 +14,8 @@ echo "---------------------------------------------------------"
# Set environment variables
INSTALL_DIR=/app # Specify the installation directory here
INSTALLER_DIR=$INSTALL_DIR/install/ubuntu24
INSTALL_SYSTEM_NAME=ubuntu24
INSTALLER_DIR=$INSTALL_DIR/install/$INSTALL_SYSTEM_NAME
# Check if script is run as root
if [[ $EUID -ne 0 ]]; then
@@ -101,5 +102,5 @@ fi
# This is where we setup the virtual environment and install dependencies
cd "$INSTALLER_DIR" || { echo "Failed to change directory to $INSTALLER_DIR"; exit 1; }
chmod +x "$INSTALLER_DIR/start.ubuntu24.sh"
"$INSTALLER_DIR/start.ubuntu24.sh"
chmod +x "$INSTALLER_DIR/start.$INSTALL_SYSTEM_NAME.sh"
"$INSTALLER_DIR/start.$INSTALL_SYSTEM_NAME.sh"

View File

@@ -10,7 +10,8 @@ echo "This script will set up and start NetAlertX on your Ubuntu24 system."
INSTALL_DIR=/app
# DO NOT CHANGE ANYTHING BELOW THIS LINE!
INSTALLER_DIR=$INSTALL_DIR/install/ubuntu24
INSTALL_SYSTEM_NAME=ubuntu24
INSTALLER_DIR=$INSTALL_DIR/install/$INSTALL_SYSTEM_NAME
CONF_FILE=app.conf
DB_FILE=app.db
NGINX_CONF_FILE=netalertx.conf
@@ -50,11 +51,12 @@ echo
# Install dependencies
apt-get install -y \
tini snmp ca-certificates curl libwww-perl arp-scan perl apt-utils cron \
nginx-light php php-cgi php-fpm php-sqlite3 php-curl sqlite3 dnsutils net-tools \
sqlite3 dnsutils net-tools mtr \
python3 python3-dev iproute2 nmap python3-pip zip usbutils traceroute nbtscan avahi-daemon avahi-utils build-essential
# alternate dependencies
apt-get install nginx nginx-core mtr php-fpm php${PHPVERSION}-fpm php-cli php${PHPVERSION} php${PHPVERSION}-sqlite3 -y
# nginx-core install nginx and nginx-common as dependencies
apt-get install nginx-core php${PHPVERSION} php${PHPVERSION}-sqlite3 php php-cgi php-fpm php-sqlite3 php-curl php-fpm php${PHPVERSION}-fpm php-cli -y
phpenmod -v ${PHPVERSION} sqlite3
update-alternatives --install /usr/bin/python python /usr/bin/python3 10
@@ -138,22 +140,31 @@ else
fi
fi
# create log and api mounts
echo "---------------------------------------------------------"
echo "[INSTALL] Create log and api mounts"
mkdir -p "${INSTALL_DIR}/log" "${INSTALL_DIR}/api"
umount "${INSTALL_DIR}/log" 2>/dev/null || true
umount "${INSTALL_DIR}/api" 2>/dev/null || true
mount -t tmpfs -o size=32m,noexec,nosuid,nodev tmpfs "${INSTALL_DIR}/log"
mount -t tmpfs -o size=16m,noexec,nosuid,nodev tmpfs "${INSTALL_DIR}/api"
# Create an empty log files
echo "---------------------------------------------------------"
echo
# Create the execution_queue.log file if it doesn't exist
echo "[INSTALL] Cleaning up old mounts if any"
umount "${INSTALL_DIR}/log"
umount "${INSTALL_DIR}/api"
echo "[INSTALL] Creating log and api folders if they don't exist"
mkdir -p "${INSTALL_DIR}/log" "${INSTALL_DIR}/api"
echo "[INSTALL] Mounting log and api folders as tmpfs"
mount -t tmpfs -o noexec,nosuid,nodev tmpfs "${INSTALL_DIR}/log"
mount -t tmpfs -o noexec,nosuid,nodev tmpfs "${INSTALL_DIR}/api"
# Create log files if they don't exist
echo "[INSTALL] Creating log files if they don't exist"
touch "${INSTALL_DIR}"/log/{app.log,execution_queue.log,app_front.log,app.php_errors.log,stderr.log,stdout.log,db_is_locked.log}
touch "${INSTALL_DIR}"/api/user_notifications.json
# Create plugins sub-directory if it doesn't exist in case a custom log folder is used
mkdir -p "${INSTALL_DIR}"/log/plugins
# Fixing file permissions
echo "[INSTALL] Fixing file permissions"
chown root:www-data "${INSTALL_DIR}"/api/user_notifications.json
@@ -182,8 +193,8 @@ fi
# Copy starter $DB_FILE and $CONF_FILE if they don't exist
cp --update=none "${INSTALL_PATH}/back/$CONF_FILE" "${INSTALL_PATH}/config/$CONF_FILE"
cp --update=none "${INSTALL_PATH}/back/$DB_FILE" "$FILEDB"
cp -u "${INSTALL_PATH}/back/$CONF_FILE" "${INSTALL_PATH}/config/$CONF_FILE"
cp -u "${INSTALL_PATH}/back/$DB_FILE" "$FILEDB"
echo "[INSTALL] Fixing permissions after copied starter config & DB"

View File

@@ -186,9 +186,15 @@ def main ():
pm.run_plugin_scripts('on_notification')
notification.setAllProcessed()
# clear pending email flag
# and the plugin events
notification.clearPendingEmailFlag()
else:
# If there are no notifications to process,
# we still need to clear all plugin events
notification.clearPluginEvents()
mylog('verbose', ['[Notification] No changes to report'])
# Commit SQL

View File

@@ -1,30 +1,31 @@
import datetime
import os
import _io
import json
import sys
import uuid
import socket
import subprocess
import requests
from yattag import indent
from json2table import convert
# Register NetAlertX directories
INSTALL_PATH="/app"
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
# Register NetAlertX modules
import conf
from const import applicationPath, logPath, apiPath, confFileName, reportTemplatesPath
from logger import logResult, mylog
from helper import generate_mac_links, removeDuplicateNewLines, timeNowTZ, get_file_content, write_file, get_setting_value, get_timezone_offset
from const import applicationPath, logPath, apiPath, reportTemplatesPath
from logger import mylog
from helper import generate_mac_links, \
removeDuplicateNewLines, \
timeNowTZ, \
write_file, \
get_setting_value, \
get_timezone_offset
from messaging.in_app import write_notification
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Notification object handling
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
class NotificationInstance:
def __init__(self, db):
self.db = db
@@ -52,14 +53,13 @@ class NotificationInstance:
return JSON, Extra
# Create a new DB entry if new notifications available, otherwise skip
def create(self, JSON, Extra=""):
JSON, Extra = self.on_before_create(JSON, Extra)
# Write output data for debug
write_file (logPath + '/report_output.json', json.dumps(JSON))
write_file(logPath + '/report_output.json', json.dumps(JSON))
# Check if nothing to report, end
if JSON["new_devices"] == [] and JSON["down_devices"] == [] and JSON["events"] == [] and JSON["plugins"] == [] and JSON["down_reconnected"] == []:
@@ -78,8 +78,6 @@ class NotificationInstance:
self.Extra = Extra
if self.HasNotifications:
# if not notiStruc.json['data'] and not notiStruc.text and not notiStruc.html:
# mylog('debug', '[Notification] notiStruc is empty')
# else:
@@ -89,7 +87,6 @@ class NotificationInstance:
HTML = ""
template_file_path = reportTemplatesPath + 'report_template.html'
# Open text Template
mylog('verbose', ['[Notification] Open text Template'])
template_file = open(reportTemplatesPath + 'report_template.txt', 'r')
@@ -105,38 +102,54 @@ class NotificationInstance:
# prepare new version text
newVersionText = ''
if conf.newVersionAvailable :
if conf.newVersionAvailable:
newVersionText = '🚀A new version is available.'
mail_text = mail_text.replace ('<NEW_VERSION>', newVersionText)
mail_html = mail_html.replace ('<NEW_VERSION>', newVersionText)
mail_text = mail_text.replace('<NEW_VERSION>', newVersionText)
mail_html = mail_html.replace('<NEW_VERSION>', newVersionText)
# Report "REPORT_DATE" in Header & footer
timeFormated = timeNowTZ().strftime ('%Y-%m-%d %H:%M')
mail_text = mail_text.replace ('<REPORT_DATE>', timeFormated)
mail_html = mail_html.replace ('<REPORT_DATE>', timeFormated)
timeFormated = timeNowTZ().strftime('%Y-%m-%d %H:%M')
mail_text = mail_text.replace('<REPORT_DATE>', timeFormated)
mail_html = mail_html.replace('<REPORT_DATE>', timeFormated)
# Report "SERVER_NAME" in Header & footer
mail_text = mail_text.replace ('<SERVER_NAME>', socket.gethostname() )
mail_html = mail_html.replace ('<SERVER_NAME>', socket.gethostname() )
mail_text = mail_text.replace('<SERVER_NAME>', socket.gethostname())
mail_html = mail_html.replace('<SERVER_NAME>', socket.gethostname())
# Report "VERSION" in Header & footer
VERSIONFILE = subprocess.check_output(['php', applicationPath + '/front/php/templates/version.php']).decode('utf-8')
mail_text = mail_text.replace ('<BUILD_VERSION>', VERSIONFILE)
mail_html = mail_html.replace ('<BUILD_VERSION>', VERSIONFILE)
try:
VERSIONFILE = subprocess.check_output(
['php', applicationPath + '/front/php/templates/version.php'],
timeout=5
).decode('utf-8')
except Exception as e:
mylog('debug', [f'[Notification] Unable to read version.php: {e}'])
VERSIONFILE = 'unknown'
mail_text = mail_text.replace('<BUILD_VERSION>', VERSIONFILE)
mail_html = mail_html.replace('<BUILD_VERSION>', VERSIONFILE)
# Report "BUILD" in Header & footer
BUILDFILE = subprocess.check_output(['php', applicationPath + '/front/php/templates/build.php']).decode('utf-8')
mail_text = mail_text.replace ('<BUILD_DATE>', BUILDFILE)
mail_html = mail_html.replace ('<BUILD_DATE>', BUILDFILE)
try:
BUILDFILE = subprocess.check_output(
['php', applicationPath + '/front/php/templates/build.php'],
timeout=5
).decode('utf-8')
except Exception as e:
mylog('debug', [f'[Notification] Unable to read build.php: {e}'])
BUILDFILE = 'unknown'
mail_text = mail_text.replace('<BUILD_DATE>', BUILDFILE)
mail_html = mail_html.replace('<BUILD_DATE>', BUILDFILE)
# Start generating the TEXT & HTML notification messages
# new_devices
# ---
html, text = construct_notifications(self.JSON, "new_devices")
mail_text = mail_text.replace ('<NEW_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace ('<NEW_DEVICES_TABLE>', html)
mail_text = mail_text.replace('<NEW_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace('<NEW_DEVICES_TABLE>', html)
mylog('verbose', ['[Notification] New Devices sections done.'])
# down_devices
@@ -144,8 +157,8 @@ class NotificationInstance:
html, text = construct_notifications(self.JSON, "down_devices")
mail_text = mail_text.replace ('<DOWN_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace ('<DOWN_DEVICES_TABLE>', html)
mail_text = mail_text.replace('<DOWN_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace('<DOWN_DEVICES_TABLE>', html)
mylog('verbose', ['[Notification] Down Devices sections done.'])
# down_reconnected
@@ -153,8 +166,8 @@ class NotificationInstance:
html, text = construct_notifications(self.JSON, "down_reconnected")
mail_text = mail_text.replace ('<DOWN_RECONNECTED_TABLE>', text + '\n')
mail_html = mail_html.replace ('<DOWN_RECONNECTED_TABLE>', html)
mail_text = mail_text.replace('<DOWN_RECONNECTED_TABLE>', text + '\n')
mail_html = mail_html.replace('<DOWN_RECONNECTED_TABLE>', html)
mylog('verbose', ['[Notification] Reconnected Down Devices sections done.'])
@@ -163,8 +176,8 @@ class NotificationInstance:
html, text = construct_notifications(self.JSON, "events")
mail_text = mail_text.replace ('<EVENTS_TABLE>', text + '\n')
mail_html = mail_html.replace ('<EVENTS_TABLE>', html)
mail_text = mail_text.replace('<EVENTS_TABLE>', text + '\n')
mail_html = mail_html.replace('<EVENTS_TABLE>', html)
mylog('verbose', ['[Notification] Events sections done.'])
@@ -172,28 +185,28 @@ class NotificationInstance:
# ---
html, text = construct_notifications(self.JSON, "plugins")
mail_text = mail_text.replace ('<PLUGINS_TABLE>', text + '\n')
mail_html = mail_html.replace ('<PLUGINS_TABLE>', html)
mail_text = mail_text.replace('<PLUGINS_TABLE>', text + '\n')
mail_html = mail_html.replace('<PLUGINS_TABLE>', html)
mylog('verbose', ['[Notification] Plugins sections done.'])
final_text = removeDuplicateNewLines(mail_text)
# Create clickable MAC links
mail_html = generate_mac_links (mail_html, conf.REPORT_DASHBOARD_URL + '/deviceDetails.php?mac=')
mail_html = generate_mac_links(mail_html, conf.REPORT_DASHBOARD_URL + '/deviceDetails.php?mac=')
final_html = indent(
mail_html,
indentation = ' ',
newline = '\r\n',
indent_text = True
indentation=' ',
newline='\r\n',
indent_text=True
)
send_api(self.JSON, final_text, final_html)
# Write output data for debug
write_file (logPath + '/report_output.txt', final_text)
write_file (logPath + '/report_output.html', final_html)
write_file(logPath + '/report_output.txt', final_text)
write_file(logPath + '/report_output.html', final_html)
mylog('minimal', ['[Notification] Udating API files'])
@@ -201,7 +214,7 @@ class NotificationInstance:
self.HTML = final_html
# Notify frontend
write_notification(f'Report:{self.GUID}', "alert", self.DateTimeCreated )
write_notification(f'Report:{self.GUID}', "alert", self.DateTimeCreated)
self.upsert()
@@ -256,57 +269,63 @@ class NotificationInstance:
self.save()
# Clear the Pending Email flag from all events and devices
def clearPendingEmailFlag(self):
# Clean Pending Alert Events
self.db.sql.execute ("""UPDATE Devices SET devLastNotification = ?
WHERE devMac IN (
SELECT eve_MAC FROM Events
WHERE eve_PendingAlertEmail = 1
)
""", (timeNowTZ(),) )
self.db.sql.execute("""
UPDATE Devices SET devLastNotification = ?
WHERE devMac IN (
SELECT eve_MAC FROM Events
WHERE eve_PendingAlertEmail = 1
)
""", (timeNowTZ(),))
self.db.sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1
AND eve_EventType !='Device Down' """)
self.db.sql.execute("""
UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1
AND eve_EventType !='Device Down' """)
# Clear down events flag after the reporting window passed
self.db.sql.execute (f"""UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1
AND eve_EventType =='Device Down'
AND eve_DateTime < datetime('now', '-{get_setting_value('NTFPRCS_alert_down_time')} minutes', '{get_timezone_offset()}')
""")
minutes = int(get_setting_value('NTFPRCS_alert_down_time') or 0)
tz_offset = get_timezone_offset()
self.db.sql.execute("""
UPDATE Events
SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1
AND eve_EventType = 'Device Down'
AND eve_DateTime < datetime('now', ?, ?)
""", (f"-{minutes} minutes", tz_offset))
mylog('minimal', ['[Notification] Notifications changes: ',
self.db.sql.rowcount])
# clear plugin events
self.db.sql.execute ("DELETE FROM Plugins_Events")
self.clearPluginEvents()
# DEBUG - print number of rows updated
mylog('minimal', ['[Notification] Notifications changes: ', self.db.sql.rowcount])
def clearPluginEvents(self):
# clear plugin events table
self.db.sql.execute("DELETE FROM Plugins_Events")
self.save()
def save(self):
# Commit changes
self.db.commitDB()
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Reporting
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
def construct_notifications(JSON, section):
jsn = JSON[section]
jsn = JSON[section]
# Return if empty
if jsn == []:
return '',''
return '', ''
tableTitle = JSON[section + "_meta"]["title"]
headers = JSON[section + "_meta"]["columnNames"]
@@ -314,14 +333,20 @@ def construct_notifications(JSON, section):
html = ''
text = ''
table_attributes = {"style" : "border-collapse: collapse; font-size: 12px; color:#70707", "width" : "100%", "cellspacing" : 0, "cellpadding" : "3px", "bordercolor" : "#C0C0C0", "border":"1"}
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:white; font-size: 16px;' bgcolor='#64a0d6' "
thProps = "width='120px' style='color:#F0F0F0' bgcolor='#64a0d6' "
build_direction = "TOP_TO_BOTTOM"
text_line = '{}\t{}\n'
if len(jsn) > 0:
text = tableTitle + "\n---------\n"
@@ -329,7 +354,13 @@ def construct_notifications(JSON, section):
html = convert({"data": jsn}, build_direction=build_direction, table_attributes=table_attributes)
# Cleanup the generated HTML table notification
html = format_table(html, "data", headerProps, tableTitle).replace('<ul>','<ul style="list-style:none;padding-left:0">').replace("<td>null</td>", "<td></td>")
html = format_table(html,
"data",
headerProps,
tableTitle).replace('<ul>',
'<ul style="list-style:none;padding-left:0">'
).replace("<td>null</td>",
"<td></td>")
# prepare text-only message
for device in jsn:
@@ -337,7 +368,7 @@ def construct_notifications(JSON, section):
padding = ""
if len(header) < 4:
padding = "\t"
text += text_line.format ( header + ': ' + padding, device[header])
text += text_line.format(header + ': ' + padding, device[header])
text += '\n'
# Format HTML table headers
@@ -346,24 +377,21 @@ def construct_notifications(JSON, section):
return html, text
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
def send_api(json_final, mail_text, mail_html):
mylog('verbose', ['[Send API] Updating notification_* files in ', apiPath])
mylog('verbose', ['[Send API] Updating notification_* files in ', apiPath])
write_file(apiPath + 'notification_text.txt' , mail_text)
write_file(apiPath + 'notification_text.html' , mail_html)
write_file(apiPath + 'notification_json_final.json' , json.dumps(json_final))
write_file(apiPath + 'notification_text.txt', mail_text)
write_file(apiPath + 'notification_text.html', mail_html)
write_file(apiPath + 'notification_json_final.json', json.dumps(json_final))
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Replacing table headers
def format_table (html, thValue, props, newThValue = ''):
def format_table(html, thValue, props, newThValue=''):
if newThValue == '':
newThValue = thValue
return html.replace("<th>"+thValue+"</th>", "<th "+props+" >"+newThValue+"</th>" )
return html.replace("<th>"+thValue+"</th>", "<th "+props+" >"+newThValue+"</th>")