mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 01:26:11 -08:00
422 lines
17 KiB
Python
Executable File
422 lines
17 KiB
Python
Executable File
import sys
|
|
import os
|
|
|
|
# Register NetAlertX directories
|
|
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
|
sys.path.extend([f"{INSTALL_PATH}/server"])
|
|
|
|
from logger import mylog # noqa: E402 [flake8 lint suppression]
|
|
from messaging.in_app import write_notification # noqa: E402 [flake8 lint suppression]
|
|
|
|
|
|
def ensure_column(sql, table: str, column_name: str, column_type: str) -> bool:
|
|
"""
|
|
Ensures a column exists in the specified table. If missing, attempts to add it.
|
|
Returns True on success, False on failure.
|
|
|
|
Parameters:
|
|
- sql: database cursor or connection wrapper (must support execute() and fetchall()).
|
|
- table: name of the table (e.g., "Devices").
|
|
- column_name: name of the column to ensure.
|
|
- column_type: SQL type of the column (e.g., "TEXT", "INTEGER", "BOOLEAN").
|
|
"""
|
|
|
|
try:
|
|
# Get actual columns from DB
|
|
sql.execute(f'PRAGMA table_info("{table}")')
|
|
actual_columns = [row[1] for row in sql.fetchall()]
|
|
|
|
# Check if target column is already present
|
|
if column_name in actual_columns:
|
|
return True # Already exists
|
|
|
|
# Define the expected columns (hardcoded base schema) [v25.5.24] - available in the default app.db
|
|
expected_columns = [
|
|
"devMac",
|
|
"devName",
|
|
"devOwner",
|
|
"devType",
|
|
"devVendor",
|
|
"devFavorite",
|
|
"devGroup",
|
|
"devComments",
|
|
"devFirstConnection",
|
|
"devLastConnection",
|
|
"devLastIP",
|
|
"devStaticIP",
|
|
"devScan",
|
|
"devLogEvents",
|
|
"devAlertEvents",
|
|
"devAlertDown",
|
|
"devSkipRepeated",
|
|
"devLastNotification",
|
|
"devPresentLastScan",
|
|
"devIsNew",
|
|
"devLocation",
|
|
"devIsArchived",
|
|
"devParentMAC",
|
|
"devParentPort",
|
|
"devIcon",
|
|
"devGUID",
|
|
"devSite",
|
|
"devSSID",
|
|
"devSyncHubNode",
|
|
"devSourcePlugin",
|
|
"devCustomProps",
|
|
]
|
|
|
|
# Check for mismatches in base schema
|
|
missing = set(expected_columns) - set(actual_columns)
|
|
extra = set(actual_columns) - set(expected_columns)
|
|
|
|
if missing:
|
|
msg = (
|
|
f"[db_upgrade] ⚠ ERROR: Unexpected DB structure "
|
|
f"(missing: {', '.join(missing) if missing else 'none'}, "
|
|
f"extra: {', '.join(extra) if extra else 'none'}) - "
|
|
"aborting schema change to prevent corruption. "
|
|
"Check https://github.com/jokob-sk/NetAlertX/blob/main/docs/UPDATES.md"
|
|
)
|
|
mylog("none", [msg])
|
|
write_notification(msg)
|
|
return False
|
|
|
|
if extra:
|
|
msg = (
|
|
f"[db_upgrade] Extra DB columns detected in {table}: {', '.join(extra)}"
|
|
)
|
|
mylog("none", [msg])
|
|
|
|
# Add missing column
|
|
mylog("verbose", [f"[db_upgrade] Adding '{column_name}' ({column_type}) to {table} table"],)
|
|
sql.execute(f'ALTER TABLE "{table}" ADD "{column_name}" {column_type}')
|
|
return True
|
|
|
|
except Exception as e:
|
|
mylog("none", [f"[db_upgrade] ERROR while adding '{column_name}': {e}"])
|
|
return False
|
|
|
|
|
|
def ensure_views(sql) -> bool:
|
|
"""
|
|
Ensures required views exist.
|
|
|
|
Parameters:
|
|
- sql: database cursor or connection wrapper (must support execute() and fetchall()).
|
|
"""
|
|
sql.execute(""" DROP VIEW IF EXISTS Events_Devices;""")
|
|
sql.execute(""" CREATE VIEW Events_Devices AS
|
|
SELECT *
|
|
FROM Events
|
|
LEFT JOIN Devices ON eve_MAC = devMac;
|
|
""")
|
|
|
|
sql.execute(""" DROP VIEW IF EXISTS LatestEventsPerMAC;""")
|
|
sql.execute("""CREATE VIEW LatestEventsPerMAC AS
|
|
WITH RankedEvents AS (
|
|
SELECT
|
|
e.*,
|
|
ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num
|
|
FROM Events AS e
|
|
)
|
|
SELECT
|
|
e.*,
|
|
d.*,
|
|
c.*
|
|
FROM RankedEvents AS e
|
|
LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac
|
|
INNER JOIN CurrentScan AS c ON e.eve_MAC = c.cur_MAC
|
|
WHERE e.row_num = 1;""")
|
|
|
|
sql.execute(""" DROP VIEW IF EXISTS Sessions_Devices;""")
|
|
sql.execute(
|
|
"""CREATE VIEW Sessions_Devices AS SELECT * FROM Sessions LEFT JOIN "Devices" ON ses_MAC = devMac;"""
|
|
)
|
|
|
|
# handling the Convert_Events_to_Sessions / Sessions screens
|
|
sql.execute("""DROP VIEW IF EXISTS Convert_Events_to_Sessions;""")
|
|
sql.execute("""CREATE VIEW Convert_Events_to_Sessions AS SELECT EVE1.eve_MAC,
|
|
EVE1.eve_IP,
|
|
EVE1.eve_EventType AS eve_EventTypeConnection,
|
|
EVE1.eve_DateTime AS eve_DateTimeConnection,
|
|
CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') OR
|
|
EVE2.eve_EventType IS NULL THEN EVE2.eve_EventType ELSE '<missing event>' END AS eve_EventTypeDisconnection,
|
|
CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') THEN EVE2.eve_DateTime ELSE NULL END AS eve_DateTimeDisconnection,
|
|
CASE WHEN EVE2.eve_EventType IS NULL THEN 1 ELSE 0 END AS eve_StillConnected,
|
|
EVE1.eve_AdditionalInfo
|
|
FROM Events AS EVE1
|
|
LEFT JOIN
|
|
Events AS EVE2 ON EVE1.eve_PairEventRowID = EVE2.RowID
|
|
WHERE EVE1.eve_EventType IN ('New Device', 'Connected','Down Reconnected')
|
|
UNION
|
|
SELECT eve_MAC,
|
|
eve_IP,
|
|
'<missing event>' AS eve_EventTypeConnection,
|
|
NULL AS eve_DateTimeConnection,
|
|
eve_EventType AS eve_EventTypeDisconnection,
|
|
eve_DateTime AS eve_DateTimeDisconnection,
|
|
0 AS eve_StillConnected,
|
|
eve_AdditionalInfo
|
|
FROM Events AS EVE1
|
|
WHERE (eve_EventType = 'Device Down' OR
|
|
eve_EventType = 'Disconnected') AND
|
|
EVE1.eve_PairEventRowID IS NULL;
|
|
""")
|
|
|
|
return True
|
|
|
|
|
|
def ensure_Indexes(sql) -> bool:
|
|
"""
|
|
Ensures required indexes exist with correct structure.
|
|
|
|
Parameters:
|
|
- sql: database cursor or connection wrapper (must support execute()).
|
|
"""
|
|
indexes = [
|
|
# Sessions
|
|
(
|
|
"idx_ses_mac_date",
|
|
"CREATE INDEX idx_ses_mac_date ON Sessions(ses_MAC, ses_DateTimeConnection, ses_DateTimeDisconnection, ses_StillConnected)",
|
|
),
|
|
# Events
|
|
(
|
|
"idx_eve_mac_date_type",
|
|
"CREATE INDEX idx_eve_mac_date_type ON Events(eve_MAC, eve_DateTime, eve_EventType)",
|
|
),
|
|
(
|
|
"idx_eve_alert_pending",
|
|
"CREATE INDEX idx_eve_alert_pending ON Events(eve_PendingAlertEmail)",
|
|
),
|
|
(
|
|
"idx_eve_mac_datetime_desc",
|
|
"CREATE INDEX idx_eve_mac_datetime_desc ON Events(eve_MAC, eve_DateTime DESC)",
|
|
),
|
|
(
|
|
"idx_eve_pairevent",
|
|
"CREATE INDEX idx_eve_pairevent ON Events(eve_PairEventRowID)",
|
|
),
|
|
(
|
|
"idx_eve_type_date",
|
|
"CREATE INDEX idx_eve_type_date ON Events(eve_EventType, eve_DateTime)",
|
|
),
|
|
# Devices
|
|
("idx_dev_mac", "CREATE INDEX idx_dev_mac ON Devices(devMac)"),
|
|
(
|
|
"idx_dev_present",
|
|
"CREATE INDEX idx_dev_present ON Devices(devPresentLastScan)",
|
|
),
|
|
(
|
|
"idx_dev_alertdown",
|
|
"CREATE INDEX idx_dev_alertdown ON Devices(devAlertDown)",
|
|
),
|
|
("idx_dev_isnew", "CREATE INDEX idx_dev_isnew ON Devices(devIsNew)"),
|
|
(
|
|
"idx_dev_isarchived",
|
|
"CREATE INDEX idx_dev_isarchived ON Devices(devIsArchived)",
|
|
),
|
|
("idx_dev_favorite", "CREATE INDEX idx_dev_favorite ON Devices(devFavorite)"),
|
|
(
|
|
"idx_dev_parentmac",
|
|
"CREATE INDEX idx_dev_parentmac ON Devices(devParentMAC)",
|
|
),
|
|
# Optional filter indexes
|
|
("idx_dev_site", "CREATE INDEX idx_dev_site ON Devices(devSite)"),
|
|
("idx_dev_group", "CREATE INDEX idx_dev_group ON Devices(devGroup)"),
|
|
("idx_dev_owner", "CREATE INDEX idx_dev_owner ON Devices(devOwner)"),
|
|
("idx_dev_type", "CREATE INDEX idx_dev_type ON Devices(devType)"),
|
|
("idx_dev_vendor", "CREATE INDEX idx_dev_vendor ON Devices(devVendor)"),
|
|
("idx_dev_location", "CREATE INDEX idx_dev_location ON Devices(devLocation)"),
|
|
# Settings
|
|
("idx_set_key", "CREATE INDEX idx_set_key ON Settings(setKey)"),
|
|
# Plugins_Objects
|
|
(
|
|
"idx_plugins_plugin_mac_ip",
|
|
"CREATE INDEX idx_plugins_plugin_mac_ip ON Plugins_Objects(Plugin, Object_PrimaryID, Object_SecondaryID)",
|
|
), # Issue #1251: Optimize name resolution lookup
|
|
]
|
|
|
|
for name, create_sql in indexes:
|
|
sql.execute(f"DROP INDEX IF EXISTS {name};")
|
|
sql.execute(create_sql + ";")
|
|
|
|
return True
|
|
|
|
|
|
def ensure_CurrentScan(sql) -> bool:
|
|
"""
|
|
Ensures required CurrentScan table exist.
|
|
|
|
Parameters:
|
|
- sql: database cursor or connection wrapper (must support execute() and fetchall()).
|
|
"""
|
|
# 🐛 CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes
|
|
sql.execute("DROP TABLE IF EXISTS CurrentScan;")
|
|
sql.execute(""" CREATE TABLE IF NOT EXISTS CurrentScan (
|
|
cur_MAC STRING(50) NOT NULL COLLATE NOCASE,
|
|
cur_IP STRING(50) NOT NULL COLLATE NOCASE,
|
|
cur_Vendor STRING(250),
|
|
cur_ScanMethod STRING(10),
|
|
cur_Name STRING(250),
|
|
cur_LastQuery STRING(250),
|
|
cur_DateTime STRING(250),
|
|
cur_SyncHubNodeName STRING(50),
|
|
cur_NetworkSite STRING(250),
|
|
cur_SSID STRING(250),
|
|
cur_NetworkNodeMAC STRING(250),
|
|
cur_PORT STRING(250),
|
|
cur_Type STRING(250)
|
|
);
|
|
""")
|
|
|
|
return True
|
|
|
|
|
|
def ensure_Parameters(sql) -> bool:
|
|
"""
|
|
Ensures required Parameters table exist.
|
|
|
|
Parameters:
|
|
- sql: database cursor or connection wrapper (must support execute() and fetchall()).
|
|
"""
|
|
|
|
# Re-creating Parameters table
|
|
mylog("verbose", ["[db_upgrade] Re-creating Parameters table"])
|
|
sql.execute("DROP TABLE Parameters;")
|
|
|
|
sql.execute("""
|
|
CREATE TABLE "Parameters" (
|
|
"par_ID" TEXT PRIMARY KEY,
|
|
"par_Value" TEXT
|
|
);
|
|
""")
|
|
|
|
return True
|
|
|
|
|
|
def ensure_Settings(sql) -> bool:
|
|
"""
|
|
Ensures required Settings table exist.
|
|
|
|
Parameters:
|
|
- sql: database cursor or connection wrapper (must support execute() and fetchall()).
|
|
"""
|
|
|
|
# Re-creating Settings table
|
|
mylog("verbose", ["[db_upgrade] Re-creating Settings table"])
|
|
|
|
sql.execute(""" DROP TABLE IF EXISTS Settings;""")
|
|
sql.execute("""
|
|
CREATE TABLE "Settings" (
|
|
"setKey" TEXT,
|
|
"setName" TEXT,
|
|
"setDescription" TEXT,
|
|
"setType" TEXT,
|
|
"setOptions" TEXT,
|
|
"setGroup" TEXT,
|
|
"setValue" TEXT,
|
|
"setEvents" TEXT,
|
|
"setOverriddenByEnv" INTEGER
|
|
);
|
|
""")
|
|
|
|
return True
|
|
|
|
|
|
def ensure_plugins_tables(sql) -> bool:
|
|
"""
|
|
Ensures required plugins tables exist.
|
|
|
|
Parameters:
|
|
- sql: database cursor or connection wrapper (must support execute() and fetchall()).
|
|
"""
|
|
|
|
# Plugin state
|
|
sql_Plugins_Objects = """ CREATE TABLE IF NOT EXISTS Plugins_Objects(
|
|
"Index" INTEGER,
|
|
Plugin TEXT NOT NULL,
|
|
Object_PrimaryID TEXT NOT NULL,
|
|
Object_SecondaryID TEXT NOT NULL,
|
|
DateTimeCreated TEXT NOT NULL,
|
|
DateTimeChanged TEXT NOT NULL,
|
|
Watched_Value1 TEXT NOT NULL,
|
|
Watched_Value2 TEXT NOT NULL,
|
|
Watched_Value3 TEXT NOT NULL,
|
|
Watched_Value4 TEXT NOT NULL,
|
|
Status TEXT NOT NULL,
|
|
Extra TEXT NOT NULL,
|
|
UserData TEXT NOT NULL,
|
|
ForeignKey TEXT NOT NULL,
|
|
SyncHubNodeName TEXT,
|
|
"HelpVal1" TEXT,
|
|
"HelpVal2" TEXT,
|
|
"HelpVal3" TEXT,
|
|
"HelpVal4" TEXT,
|
|
ObjectGUID TEXT,
|
|
PRIMARY KEY("Index" AUTOINCREMENT)
|
|
); """
|
|
sql.execute(sql_Plugins_Objects)
|
|
|
|
# Plugin execution results
|
|
sql_Plugins_Events = """ CREATE TABLE IF NOT EXISTS Plugins_Events(
|
|
"Index" INTEGER,
|
|
Plugin TEXT NOT NULL,
|
|
Object_PrimaryID TEXT NOT NULL,
|
|
Object_SecondaryID TEXT NOT NULL,
|
|
DateTimeCreated TEXT NOT NULL,
|
|
DateTimeChanged TEXT NOT NULL,
|
|
Watched_Value1 TEXT NOT NULL,
|
|
Watched_Value2 TEXT NOT NULL,
|
|
Watched_Value3 TEXT NOT NULL,
|
|
Watched_Value4 TEXT NOT NULL,
|
|
Status TEXT NOT NULL,
|
|
Extra TEXT NOT NULL,
|
|
UserData TEXT NOT NULL,
|
|
ForeignKey TEXT NOT NULL,
|
|
SyncHubNodeName TEXT,
|
|
"HelpVal1" TEXT,
|
|
"HelpVal2" TEXT,
|
|
"HelpVal3" TEXT,
|
|
"HelpVal4" TEXT,
|
|
PRIMARY KEY("Index" AUTOINCREMENT)
|
|
); """
|
|
sql.execute(sql_Plugins_Events)
|
|
|
|
# Plugin execution history
|
|
sql_Plugins_History = """ CREATE TABLE IF NOT EXISTS Plugins_History(
|
|
"Index" INTEGER,
|
|
Plugin TEXT NOT NULL,
|
|
Object_PrimaryID TEXT NOT NULL,
|
|
Object_SecondaryID TEXT NOT NULL,
|
|
DateTimeCreated TEXT NOT NULL,
|
|
DateTimeChanged TEXT NOT NULL,
|
|
Watched_Value1 TEXT NOT NULL,
|
|
Watched_Value2 TEXT NOT NULL,
|
|
Watched_Value3 TEXT NOT NULL,
|
|
Watched_Value4 TEXT NOT NULL,
|
|
Status TEXT NOT NULL,
|
|
Extra TEXT NOT NULL,
|
|
UserData TEXT NOT NULL,
|
|
ForeignKey TEXT NOT NULL,
|
|
SyncHubNodeName TEXT,
|
|
"HelpVal1" TEXT,
|
|
"HelpVal2" TEXT,
|
|
"HelpVal3" TEXT,
|
|
"HelpVal4" TEXT,
|
|
PRIMARY KEY("Index" AUTOINCREMENT)
|
|
); """
|
|
sql.execute(sql_Plugins_History)
|
|
|
|
# Dynamically generated language strings
|
|
sql.execute("DROP TABLE IF EXISTS Plugins_Language_Strings;")
|
|
sql.execute(""" CREATE TABLE IF NOT EXISTS Plugins_Language_Strings(
|
|
"Index" INTEGER,
|
|
Language_Code TEXT NOT NULL,
|
|
String_Key TEXT NOT NULL,
|
|
String_Value TEXT NOT NULL,
|
|
Extra TEXT NOT NULL,
|
|
PRIMARY KEY("Index" AUTOINCREMENT)
|
|
); """)
|
|
|
|
return True
|