mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
/data and /tmp standarization
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
import sys
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import if_byte_then_to_str
|
||||
from logger import mylog
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# -------------------------------------------------------------------------------
|
||||
# Return the SQL WHERE clause for filtering devices based on their status.
|
||||
|
||||
|
||||
def get_device_condition_by_status(device_status):
|
||||
"""
|
||||
Return the SQL WHERE clause for filtering devices based on their status.
|
||||
@@ -31,18 +33,18 @@ def get_device_condition_by_status(device_status):
|
||||
Defaults to 'WHERE 1=0' for unrecognized statuses.
|
||||
"""
|
||||
conditions = {
|
||||
'all': 'WHERE devIsArchived=0',
|
||||
'my': 'WHERE devIsArchived=0',
|
||||
'connected': 'WHERE devIsArchived=0 AND devPresentLastScan=1',
|
||||
'favorites': 'WHERE devIsArchived=0 AND devFavorite=1',
|
||||
'new': 'WHERE devIsArchived=0 AND devIsNew=1',
|
||||
'down': 'WHERE devIsArchived=0 AND devAlertDown != 0 AND devPresentLastScan=0',
|
||||
'archived': 'WHERE devIsArchived=1'
|
||||
"all": "WHERE devIsArchived=0",
|
||||
"my": "WHERE devIsArchived=0",
|
||||
"connected": "WHERE devIsArchived=0 AND devPresentLastScan=1",
|
||||
"favorites": "WHERE devIsArchived=0 AND devFavorite=1",
|
||||
"new": "WHERE devIsArchived=0 AND devIsNew=1",
|
||||
"down": "WHERE devIsArchived=0 AND devAlertDown != 0 AND devPresentLastScan=0",
|
||||
"archived": "WHERE devIsArchived=1",
|
||||
}
|
||||
return conditions.get(device_status, 'WHERE 1=0')
|
||||
return conditions.get(device_status, "WHERE 1=0")
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# -------------------------------------------------------------------------------
|
||||
# Creates a JSON-like dictionary from a database row
|
||||
def row_to_json(names, row):
|
||||
"""
|
||||
@@ -57,7 +59,7 @@ def row_to_json(names, row):
|
||||
dict: A dictionary where keys are column names and values are the corresponding
|
||||
row values. Byte values are automatically converted to strings using
|
||||
`if_byte_then_to_str`.
|
||||
|
||||
|
||||
Example:
|
||||
names = ['id', 'name', 'data']
|
||||
row = {0: 1, 1: b'Example', 2: b'\x01\x02'}
|
||||
@@ -72,7 +74,7 @@ def row_to_json(names, row):
|
||||
return rowEntry
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# -------------------------------------------------------------------------------
|
||||
def sanitize_SQL_input(val):
|
||||
"""
|
||||
Sanitize a value for use in SQL queries by replacing single quotes in strings.
|
||||
@@ -81,19 +83,19 @@ def sanitize_SQL_input(val):
|
||||
val (any): The value to sanitize.
|
||||
|
||||
Returns:
|
||||
str or any:
|
||||
str or any:
|
||||
- Returns an empty string if val is None.
|
||||
- Returns a string with single quotes replaced by underscores if val is a string.
|
||||
- Returns val unchanged if it is any other type.
|
||||
"""
|
||||
if val is None:
|
||||
return ''
|
||||
return ""
|
||||
if isinstance(val, str):
|
||||
return val.replace("'", "_")
|
||||
return val # Return non-string values as they are
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def get_date_from_period(period):
|
||||
"""
|
||||
Convert a period string into an SQLite date expression.
|
||||
@@ -105,10 +107,10 @@ def get_date_from_period(period):
|
||||
str: An SQLite date expression like "date('now', '-7 day')" corresponding to the period.
|
||||
"""
|
||||
days_map = {
|
||||
'7 days': 7,
|
||||
'1 month': 30,
|
||||
'1 year': 365,
|
||||
'100 years': 3650, # actually 10 years in original PHP
|
||||
"7 days": 7,
|
||||
"1 month": 30,
|
||||
"1 year": 365,
|
||||
"100 years": 3650, # actually 10 years in original PHP
|
||||
}
|
||||
|
||||
days = days_map.get(period, 1) # default 1 day
|
||||
@@ -117,7 +119,7 @@ def get_date_from_period(period):
|
||||
return period_sql
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# -------------------------------------------------------------------------------
|
||||
def print_table_schema(db, table):
|
||||
"""
|
||||
Print the schema of a database table to the log.
|
||||
@@ -134,20 +136,23 @@ def print_table_schema(db, table):
|
||||
result = sql.fetchall()
|
||||
|
||||
if not result:
|
||||
mylog('none', f'[Schema] Table "{table}" not found or has no columns.')
|
||||
mylog("none", f'[Schema] Table "{table}" not found or has no columns.')
|
||||
return
|
||||
|
||||
mylog('debug', f'[Schema] Structure for table: {table}')
|
||||
header = f"{'cid':<4} {'name':<20} {'type':<10} {'notnull':<8} {'default':<10} {'pk':<2}"
|
||||
mylog('debug', header)
|
||||
mylog('debug', '-' * len(header))
|
||||
mylog("debug", f"[Schema] Structure for table: {table}")
|
||||
header = (
|
||||
f"{'cid':<4} {'name':<20} {'type':<10} {'notnull':<8} {'default':<10} {'pk':<2}"
|
||||
)
|
||||
mylog("debug", header)
|
||||
mylog("debug", "-" * len(header))
|
||||
|
||||
for row in result:
|
||||
# row = (cid, name, type, notnull, dflt_value, pk)
|
||||
line = f"{row[0]:<4} {row[1]:<20} {row[2]:<10} {row[3]:<8} {str(row[4]):<10} {row[5]:<2}"
|
||||
mylog('debug', line)
|
||||
mylog("debug", line)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# Generate a WHERE condition for SQLite based on a list of values.
|
||||
def list_to_where(logical_operator, column_name, condition_operator, values_list):
|
||||
"""
|
||||
@@ -177,9 +182,10 @@ def list_to_where(logical_operator, column_name, condition_operator, values_list
|
||||
for value in values_list[1:]:
|
||||
condition += f" {logical_operator} {column_name} {condition_operator} '{value}'"
|
||||
|
||||
return f'({condition})'
|
||||
return f"({condition})"
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
def get_table_json(sql, sql_query, parameters=None):
|
||||
"""
|
||||
Execute a SQL query and return the results as JSON-like dict.
|
||||
@@ -198,22 +204,23 @@ def get_table_json(sql, sql_query, parameters=None):
|
||||
else:
|
||||
sql.execute(sql_query)
|
||||
rows = sql.fetchall()
|
||||
if (rows):
|
||||
if rows:
|
||||
# We only return data if we actually got some out of SQLite
|
||||
column_names = [col[0] for col in sql.description]
|
||||
data = [row_to_json(column_names, row) for row in rows]
|
||||
return json_obj({"data": data}, column_names)
|
||||
except sqlite3.Error as e:
|
||||
# SQLite error, e.g. malformed query
|
||||
mylog('verbose', ['[Database] - SQL ERROR: ', e])
|
||||
mylog("verbose", ["[Database] - SQL ERROR: ", e])
|
||||
except Exception as e:
|
||||
# Catch-all for other exceptions, e.g. iteration error
|
||||
mylog('verbose', ['[Database] - Unexpected ERROR: ', e])
|
||||
|
||||
mylog("verbose", ["[Database] - Unexpected ERROR: ", e])
|
||||
|
||||
# In case of any error or no data, return empty object
|
||||
return json_obj({"data": []}, [])
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
class json_obj:
|
||||
"""
|
||||
A wrapper class for JSON-style objects returned from database queries.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
from logger import mylog
|
||||
@@ -12,7 +13,7 @@ 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").
|
||||
@@ -31,14 +32,37 @@ def ensure_column(sql, table: str, column_name: str, column_type: str) -> bool:
|
||||
|
||||
# Define the expected columns (hardcoded base schema) [v25.5.24] - available in teh 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'
|
||||
"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
|
||||
@@ -46,46 +70,52 @@ def ensure_column(sql, table: str, column_name: str, column_type: str) -> bool:
|
||||
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])
|
||||
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])
|
||||
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"])
|
||||
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}"])
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
sql.execute(""" DROP VIEW IF EXISTS LatestEventsPerMAC;""")
|
||||
sql.execute("""CREATE VIEW LatestEventsPerMAC AS
|
||||
WITH RankedEvents AS (
|
||||
SELECT
|
||||
e.*,
|
||||
@@ -100,11 +130,13 @@ def ensure_views(sql) -> bool:
|
||||
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;""")
|
||||
|
||||
sql.execute(""" CREATE VIEW IF NOT EXISTS LatestEventsPerMAC AS
|
||||
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;"""
|
||||
)
|
||||
|
||||
sql.execute(""" CREATE VIEW IF NOT EXISTS LatestEventsPerMAC AS
|
||||
WITH RankedEvents AS (
|
||||
SELECT
|
||||
e.*,
|
||||
@@ -121,9 +153,9 @@ def ensure_views(sql) -> bool:
|
||||
WHERE e.row_num = 1;
|
||||
""")
|
||||
|
||||
# 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,
|
||||
# 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,
|
||||
@@ -151,7 +183,8 @@ def ensure_views(sql) -> bool:
|
||||
EVE1.eve_PairEventRowID IS NULL;
|
||||
""")
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def ensure_Indexes(sql) -> bool:
|
||||
"""
|
||||
@@ -162,30 +195,51 @@ def ensure_Indexes(sql) -> bool:
|
||||
"""
|
||||
indexes = [
|
||||
# Sessions
|
||||
("idx_ses_mac_date",
|
||||
"CREATE INDEX idx_ses_mac_date ON Sessions(ses_MAC, ses_DateTimeConnection, ses_DateTimeDisconnection, ses_StillConnected)"),
|
||||
|
||||
(
|
||||
"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)"),
|
||||
|
||||
(
|
||||
"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_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_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)"),
|
||||
|
||||
(
|
||||
"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)"),
|
||||
@@ -193,12 +247,13 @@ def ensure_Indexes(sql) -> bool:
|
||||
("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
|
||||
(
|
||||
"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:
|
||||
@@ -208,19 +263,16 @@ def ensure_Indexes(sql) -> bool:
|
||||
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 (
|
||||
"""
|
||||
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),
|
||||
@@ -237,42 +289,44 @@ def ensure_CurrentScan(sql) -> bool:
|
||||
);
|
||||
""")
|
||||
|
||||
return True
|
||||
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;")
|
||||
"""
|
||||
Ensures required Parameters table exist.
|
||||
|
||||
sql.execute("""
|
||||
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
|
||||
""")
|
||||
|
||||
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"])
|
||||
"""
|
||||
Ensures required Settings table exist.
|
||||
|
||||
sql.execute(""" DROP TABLE IF EXISTS Settings;""")
|
||||
sql.execute("""
|
||||
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,
|
||||
@@ -284,21 +338,21 @@ def ensure_Settings(sql) -> bool:
|
||||
"setEvents" TEXT,
|
||||
"setOverriddenByEnv" INTEGER
|
||||
);
|
||||
""")
|
||||
""")
|
||||
|
||||
return True
|
||||
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(
|
||||
"""
|
||||
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,
|
||||
@@ -321,10 +375,10 @@ def ensure_plugins_tables(sql) -> bool:
|
||||
ObjectGUID TEXT,
|
||||
PRIMARY KEY("Index" AUTOINCREMENT)
|
||||
); """
|
||||
sql.execute(sql_Plugins_Objects)
|
||||
sql.execute(sql_Plugins_Objects)
|
||||
|
||||
# Plugin execution results
|
||||
sql_Plugins_Events = """ CREATE TABLE IF NOT EXISTS Plugins_Events(
|
||||
# Plugin execution results
|
||||
sql_Plugins_Events = """ CREATE TABLE IF NOT EXISTS Plugins_Events(
|
||||
"Index" INTEGER,
|
||||
Plugin TEXT NOT NULL,
|
||||
Object_PrimaryID TEXT NOT NULL,
|
||||
@@ -346,10 +400,10 @@ def ensure_plugins_tables(sql) -> bool:
|
||||
"HelpVal4" TEXT,
|
||||
PRIMARY KEY("Index" AUTOINCREMENT)
|
||||
); """
|
||||
sql.execute(sql_Plugins_Events)
|
||||
sql.execute(sql_Plugins_Events)
|
||||
|
||||
# Plugin execution history
|
||||
sql_Plugins_History = """ CREATE TABLE IF NOT EXISTS Plugins_History(
|
||||
# Plugin execution history
|
||||
sql_Plugins_History = """ CREATE TABLE IF NOT EXISTS Plugins_History(
|
||||
"Index" INTEGER,
|
||||
Plugin TEXT NOT NULL,
|
||||
Object_PrimaryID TEXT NOT NULL,
|
||||
@@ -371,11 +425,11 @@ def ensure_plugins_tables(sql) -> bool:
|
||||
"HelpVal4" TEXT,
|
||||
PRIMARY KEY("Index" AUTOINCREMENT)
|
||||
); """
|
||||
sql.execute(sql_Plugins_History)
|
||||
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(
|
||||
# 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,
|
||||
@@ -384,4 +438,4 @@ def ensure_plugins_tables(sql) -> bool:
|
||||
PRIMARY KEY("Index" AUTOINCREMENT)
|
||||
); """)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
@@ -11,10 +11,11 @@ License: GNU GPLv3
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from typing import Dict, List, Tuple, Any, Optional
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH = "/app"
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
from logger import mylog
|
||||
@@ -28,27 +29,59 @@ class SafeConditionBuilder:
|
||||
|
||||
# Whitelist of allowed column names for filtering
|
||||
ALLOWED_COLUMNS = {
|
||||
'eve_MAC', 'eve_DateTime', 'eve_IP', 'eve_EventType', 'devName',
|
||||
'devComments', 'devLastIP', 'devVendor', 'devAlertEvents',
|
||||
'devAlertDown', 'devIsArchived', 'devPresentLastScan', 'devFavorite',
|
||||
'devIsNew', 'Plugin', 'Object_PrimaryId', 'Object_SecondaryId',
|
||||
'DateTimeChanged', 'Watched_Value1', 'Watched_Value2', 'Watched_Value3',
|
||||
'Watched_Value4', 'Status'
|
||||
"eve_MAC",
|
||||
"eve_DateTime",
|
||||
"eve_IP",
|
||||
"eve_EventType",
|
||||
"devName",
|
||||
"devComments",
|
||||
"devLastIP",
|
||||
"devVendor",
|
||||
"devAlertEvents",
|
||||
"devAlertDown",
|
||||
"devIsArchived",
|
||||
"devPresentLastScan",
|
||||
"devFavorite",
|
||||
"devIsNew",
|
||||
"Plugin",
|
||||
"Object_PrimaryId",
|
||||
"Object_SecondaryId",
|
||||
"DateTimeChanged",
|
||||
"Watched_Value1",
|
||||
"Watched_Value2",
|
||||
"Watched_Value3",
|
||||
"Watched_Value4",
|
||||
"Status",
|
||||
}
|
||||
|
||||
# Whitelist of allowed comparison operators
|
||||
ALLOWED_OPERATORS = {
|
||||
'=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE',
|
||||
'IN', 'NOT IN', 'IS NULL', 'IS NOT NULL'
|
||||
"=",
|
||||
"!=",
|
||||
"<>",
|
||||
"<",
|
||||
">",
|
||||
"<=",
|
||||
">=",
|
||||
"LIKE",
|
||||
"NOT LIKE",
|
||||
"IN",
|
||||
"NOT IN",
|
||||
"IS NULL",
|
||||
"IS NOT NULL",
|
||||
}
|
||||
|
||||
# Whitelist of allowed logical operators
|
||||
ALLOWED_LOGICAL_OPERATORS = {'AND', 'OR'}
|
||||
ALLOWED_LOGICAL_OPERATORS = {"AND", "OR"}
|
||||
|
||||
# Whitelist of allowed event types
|
||||
ALLOWED_EVENT_TYPES = {
|
||||
'New Device', 'Connected', 'Disconnected', 'Device Down',
|
||||
'Down Reconnected', 'IP Changed'
|
||||
"New Device",
|
||||
"Connected",
|
||||
"Disconnected",
|
||||
"Device Down",
|
||||
"Down Reconnected",
|
||||
"IP Changed",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
@@ -56,7 +89,7 @@ class SafeConditionBuilder:
|
||||
self.parameters = {}
|
||||
self.param_counter = 0
|
||||
|
||||
def _generate_param_name(self, prefix: str = 'param') -> str:
|
||||
def _generate_param_name(self, prefix: str = "param") -> str:
|
||||
"""Generate a unique parameter name for SQL binding."""
|
||||
self.param_counter += 1
|
||||
return f"{prefix}_{self.param_counter}"
|
||||
@@ -64,32 +97,32 @@ class SafeConditionBuilder:
|
||||
def _sanitize_string(self, value: str) -> str:
|
||||
"""
|
||||
Sanitize string input by removing potentially dangerous characters.
|
||||
|
||||
|
||||
Args:
|
||||
value: String to sanitize
|
||||
|
||||
|
||||
Returns:
|
||||
Sanitized string
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return str(value)
|
||||
|
||||
|
||||
# Replace {s-quote} placeholder with single quote (maintaining compatibility)
|
||||
value = value.replace('{s-quote}', "'")
|
||||
|
||||
value = value.replace("{s-quote}", "'")
|
||||
|
||||
# Remove any null bytes, control characters, and excessive whitespace
|
||||
value = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f]', '', value)
|
||||
value = re.sub(r'\s+', ' ', value.strip())
|
||||
|
||||
value = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f]", "", value)
|
||||
value = re.sub(r"\s+", " ", value.strip())
|
||||
|
||||
return value
|
||||
|
||||
def _validate_column_name(self, column: str) -> bool:
|
||||
"""
|
||||
Validate that a column name is in the whitelist.
|
||||
|
||||
|
||||
Args:
|
||||
column: Column name to validate
|
||||
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
@@ -98,10 +131,10 @@ class SafeConditionBuilder:
|
||||
def _validate_operator(self, operator: str) -> bool:
|
||||
"""
|
||||
Validate that an operator is in the whitelist.
|
||||
|
||||
|
||||
Args:
|
||||
operator: Operator to validate
|
||||
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
@@ -110,10 +143,10 @@ class SafeConditionBuilder:
|
||||
def _validate_logical_operator(self, logical_op: str) -> bool:
|
||||
"""
|
||||
Validate that a logical operator is in the whitelist.
|
||||
|
||||
|
||||
Args:
|
||||
logical_op: Logical operator to validate
|
||||
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
@@ -124,13 +157,13 @@ class SafeConditionBuilder:
|
||||
Parse and build a safe SQL condition from a user-provided string.
|
||||
This method attempts to parse common condition patterns and convert
|
||||
them to parameterized queries.
|
||||
|
||||
|
||||
Args:
|
||||
condition_string: User-provided condition string
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (safe_sql_snippet, parameters_dict)
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If the condition contains invalid or unsafe elements
|
||||
"""
|
||||
@@ -139,7 +172,7 @@ class SafeConditionBuilder:
|
||||
|
||||
# Sanitize the input
|
||||
condition_string = self._sanitize_string(condition_string)
|
||||
|
||||
|
||||
# Reset parameters for this condition
|
||||
self.parameters = {}
|
||||
self.param_counter = 0
|
||||
@@ -147,7 +180,7 @@ class SafeConditionBuilder:
|
||||
try:
|
||||
return self._parse_condition(condition_string)
|
||||
except Exception as e:
|
||||
mylog('verbose', f'[SafeConditionBuilder] Error parsing condition: {e}')
|
||||
mylog("verbose", f"[SafeConditionBuilder] Error parsing condition: {e}")
|
||||
raise ValueError(f"Invalid condition format: {condition_string}")
|
||||
|
||||
def _parse_condition(self, condition: str) -> Tuple[str, Dict[str, Any]]:
|
||||
@@ -180,12 +213,16 @@ class SafeConditionBuilder:
|
||||
clause_text = condition
|
||||
|
||||
# Check for leading AND
|
||||
if condition.upper().startswith('AND ') or condition.upper().startswith('AND\t'):
|
||||
logical_op = 'AND'
|
||||
if condition.upper().startswith("AND ") or condition.upper().startswith(
|
||||
"AND\t"
|
||||
):
|
||||
logical_op = "AND"
|
||||
clause_text = condition[3:].strip()
|
||||
# Check for leading OR
|
||||
elif condition.upper().startswith('OR ') or condition.upper().startswith('OR\t'):
|
||||
logical_op = 'OR'
|
||||
elif condition.upper().startswith("OR ") or condition.upper().startswith(
|
||||
"OR\t"
|
||||
):
|
||||
logical_op = "OR"
|
||||
clause_text = condition[2:].strip()
|
||||
|
||||
# Parse the single condition
|
||||
@@ -224,13 +261,13 @@ class SafeConditionBuilder:
|
||||
remaining = condition[i:].upper()
|
||||
|
||||
# Check for AND (must be word boundary)
|
||||
if remaining.startswith('AND ') or remaining.startswith('AND\t'):
|
||||
if remaining.startswith("AND ") or remaining.startswith("AND\t"):
|
||||
logical_op_count += 1
|
||||
i += 3
|
||||
continue
|
||||
|
||||
# Check for OR (must be word boundary)
|
||||
if remaining.startswith('OR ') or remaining.startswith('OR\t'):
|
||||
if remaining.startswith("OR ") or remaining.startswith("OR\t"):
|
||||
logical_op_count += 1
|
||||
i += 2
|
||||
continue
|
||||
@@ -277,7 +314,9 @@ class SafeConditionBuilder:
|
||||
|
||||
return final_sql, all_params
|
||||
|
||||
def _split_by_logical_operators(self, condition: str) -> List[Tuple[str, Optional[str]]]:
|
||||
def _split_by_logical_operators(
|
||||
self, condition: str
|
||||
) -> List[Tuple[str, Optional[str]]]:
|
||||
"""
|
||||
Split a compound condition into individual clauses.
|
||||
|
||||
@@ -311,41 +350,45 @@ class SafeConditionBuilder:
|
||||
remaining = condition[i:].upper()
|
||||
|
||||
# Check if we're at a word boundary (start of string or after whitespace)
|
||||
at_word_boundary = (i == 0 or condition[i-1] in ' \t')
|
||||
at_word_boundary = i == 0 or condition[i - 1] in " \t"
|
||||
|
||||
# Check for AND (must be at word boundary)
|
||||
if at_word_boundary and (remaining.startswith('AND ') or remaining.startswith('AND\t')):
|
||||
if at_word_boundary and (
|
||||
remaining.startswith("AND ") or remaining.startswith("AND\t")
|
||||
):
|
||||
# Save current clause if we have one
|
||||
if current_clause:
|
||||
clause_text = ''.join(current_clause).strip()
|
||||
clause_text = "".join(current_clause).strip()
|
||||
if clause_text:
|
||||
clauses.append((clause_text, current_logical_op))
|
||||
current_clause = []
|
||||
|
||||
# Set the logical operator for the next clause
|
||||
current_logical_op = 'AND'
|
||||
current_logical_op = "AND"
|
||||
i += 3 # Skip 'AND'
|
||||
|
||||
# Skip whitespace after AND
|
||||
while i < len(condition) and condition[i] in ' \t':
|
||||
while i < len(condition) and condition[i] in " \t":
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check for OR (must be at word boundary)
|
||||
if at_word_boundary and (remaining.startswith('OR ') or remaining.startswith('OR\t')):
|
||||
if at_word_boundary and (
|
||||
remaining.startswith("OR ") or remaining.startswith("OR\t")
|
||||
):
|
||||
# Save current clause if we have one
|
||||
if current_clause:
|
||||
clause_text = ''.join(current_clause).strip()
|
||||
clause_text = "".join(current_clause).strip()
|
||||
if clause_text:
|
||||
clauses.append((clause_text, current_logical_op))
|
||||
current_clause = []
|
||||
|
||||
# Set the logical operator for the next clause
|
||||
current_logical_op = 'OR'
|
||||
current_logical_op = "OR"
|
||||
i += 2 # Skip 'OR'
|
||||
|
||||
# Skip whitespace after OR
|
||||
while i < len(condition) and condition[i] in ' \t':
|
||||
while i < len(condition) and condition[i] in " \t":
|
||||
i += 1
|
||||
continue
|
||||
|
||||
@@ -355,13 +398,15 @@ class SafeConditionBuilder:
|
||||
|
||||
# Don't forget the last clause
|
||||
if current_clause:
|
||||
clause_text = ''.join(current_clause).strip()
|
||||
clause_text = "".join(current_clause).strip()
|
||||
if clause_text:
|
||||
clauses.append((clause_text, current_logical_op))
|
||||
|
||||
return clauses
|
||||
|
||||
def _parse_single_condition(self, condition: str, logical_op: Optional[str] = None) -> Tuple[str, Dict[str, Any]]:
|
||||
def _parse_single_condition(
|
||||
self, condition: str, logical_op: Optional[str] = None
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""
|
||||
Parse a single condition clause into safe SQL with parameters.
|
||||
|
||||
@@ -385,7 +430,7 @@ class SafeConditionBuilder:
|
||||
|
||||
# Simple pattern matching for common conditions
|
||||
# Pattern 1: [AND/OR] column operator value (supporting Unicode in quoted strings)
|
||||
pattern1 = r'^\s*(\w+)\s+(=|!=|<>|<|>|<=|>=|LIKE|NOT\s+LIKE)\s+\'([^\']*)\'\s*$'
|
||||
pattern1 = r"^\s*(\w+)\s+(=|!=|<>|<|>|<=|>=|LIKE|NOT\s+LIKE)\s+\'([^\']*)\'\s*$"
|
||||
match1 = re.match(pattern1, condition, re.IGNORECASE | re.UNICODE)
|
||||
|
||||
if match1:
|
||||
@@ -393,7 +438,7 @@ class SafeConditionBuilder:
|
||||
return self._build_simple_condition(logical_op, column, operator, value)
|
||||
|
||||
# Pattern 2: [AND/OR] column IN ('val1', 'val2', ...)
|
||||
pattern2 = r'^\s*(\w+)\s+(IN|NOT\s+IN)\s+\(([^)]+)\)\s*$'
|
||||
pattern2 = r"^\s*(\w+)\s+(IN|NOT\s+IN)\s+\(([^)]+)\)\s*$"
|
||||
match2 = re.match(pattern2, condition, re.IGNORECASE)
|
||||
|
||||
if match2:
|
||||
@@ -401,7 +446,7 @@ class SafeConditionBuilder:
|
||||
return self._build_in_condition(logical_op, column, operator, values_str)
|
||||
|
||||
# Pattern 3: [AND/OR] column IS NULL/IS NOT NULL
|
||||
pattern3 = r'^\s*(\w+)\s+(IS\s+NULL|IS\s+NOT\s+NULL)\s*$'
|
||||
pattern3 = r"^\s*(\w+)\s+(IS\s+NULL|IS\s+NOT\s+NULL)\s*$"
|
||||
match3 = re.match(pattern3, condition, re.IGNORECASE)
|
||||
|
||||
if match3:
|
||||
@@ -411,16 +456,17 @@ class SafeConditionBuilder:
|
||||
# If no patterns match, reject the condition for security
|
||||
raise ValueError(f"Unsupported condition pattern: {condition}")
|
||||
|
||||
def _build_simple_condition(self, logical_op: Optional[str], column: str,
|
||||
operator: str, value: str) -> Tuple[str, Dict[str, Any]]:
|
||||
def _build_simple_condition(
|
||||
self, logical_op: Optional[str], column: str, operator: str, value: str
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""Build a simple condition with parameter binding."""
|
||||
# Validate components
|
||||
if not self._validate_column_name(column):
|
||||
raise ValueError(f"Invalid column name: {column}")
|
||||
|
||||
|
||||
if not self._validate_operator(operator):
|
||||
raise ValueError(f"Invalid operator: {operator}")
|
||||
|
||||
|
||||
if logical_op and not self._validate_logical_operator(logical_op):
|
||||
raise ValueError(f"Invalid logical operator: {logical_op}")
|
||||
|
||||
@@ -432,18 +478,19 @@ class SafeConditionBuilder:
|
||||
sql_parts = []
|
||||
if logical_op:
|
||||
sql_parts.append(logical_op.upper())
|
||||
|
||||
|
||||
sql_parts.extend([column, operator.upper(), f":{param_name}"])
|
||||
|
||||
|
||||
return " ".join(sql_parts), self.parameters
|
||||
|
||||
def _build_in_condition(self, logical_op: Optional[str], column: str,
|
||||
operator: str, values_str: str) -> Tuple[str, Dict[str, Any]]:
|
||||
def _build_in_condition(
|
||||
self, logical_op: Optional[str], column: str, operator: str, values_str: str
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""Build an IN condition with parameter binding."""
|
||||
# Validate components
|
||||
if not self._validate_column_name(column):
|
||||
raise ValueError(f"Invalid column name: {column}")
|
||||
|
||||
|
||||
if logical_op and not self._validate_logical_operator(logical_op):
|
||||
raise ValueError(f"Invalid logical operator: {logical_op}")
|
||||
|
||||
@@ -452,7 +499,7 @@ class SafeConditionBuilder:
|
||||
# Simple regex to extract quoted values
|
||||
value_pattern = r"'([^']*)'"
|
||||
matches = re.findall(value_pattern, values_str)
|
||||
|
||||
|
||||
if not matches:
|
||||
raise ValueError("No valid values found in IN clause")
|
||||
|
||||
@@ -467,18 +514,19 @@ class SafeConditionBuilder:
|
||||
sql_parts = []
|
||||
if logical_op:
|
||||
sql_parts.append(logical_op.upper())
|
||||
|
||||
|
||||
sql_parts.extend([column, operator.upper(), f"({', '.join(param_names)})"])
|
||||
|
||||
|
||||
return " ".join(sql_parts), self.parameters
|
||||
|
||||
def _build_null_condition(self, logical_op: Optional[str], column: str,
|
||||
operator: str) -> Tuple[str, Dict[str, Any]]:
|
||||
def _build_null_condition(
|
||||
self, logical_op: Optional[str], column: str, operator: str
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""Build a NULL check condition."""
|
||||
# Validate components
|
||||
if not self._validate_column_name(column):
|
||||
raise ValueError(f"Invalid column name: {column}")
|
||||
|
||||
|
||||
if logical_op and not self._validate_logical_operator(logical_op):
|
||||
raise ValueError(f"Invalid logical operator: {logical_op}")
|
||||
|
||||
@@ -486,18 +534,18 @@ class SafeConditionBuilder:
|
||||
sql_parts = []
|
||||
if logical_op:
|
||||
sql_parts.append(logical_op.upper())
|
||||
|
||||
|
||||
sql_parts.extend([column, operator.upper()])
|
||||
|
||||
|
||||
return " ".join(sql_parts), {}
|
||||
|
||||
def build_device_name_filter(self, device_name: str) -> Tuple[str, Dict[str, Any]]:
|
||||
"""
|
||||
Build a safe device name filter condition.
|
||||
|
||||
|
||||
Args:
|
||||
device_name: Device name to filter for
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (safe_sql_snippet, parameters_dict)
|
||||
"""
|
||||
@@ -505,74 +553,86 @@ class SafeConditionBuilder:
|
||||
return "", {}
|
||||
|
||||
device_name = self._sanitize_string(device_name)
|
||||
param_name = self._generate_param_name('device_name')
|
||||
param_name = self._generate_param_name("device_name")
|
||||
self.parameters[param_name] = device_name
|
||||
|
||||
return f"AND devName = :{param_name}", self.parameters
|
||||
|
||||
def build_condition(self, conditions: List[Dict[str, str]], logical_operator: str = "AND") -> Tuple[str, Dict[str, Any]]:
|
||||
def build_condition(
|
||||
self, conditions: List[Dict[str, str]], logical_operator: str = "AND"
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""
|
||||
Build a safe SQL condition from a list of condition dictionaries.
|
||||
|
||||
|
||||
Args:
|
||||
conditions: List of condition dicts with 'column', 'operator', 'value' keys
|
||||
logical_operator: Logical operator to join conditions (AND/OR)
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (safe_sql_snippet, parameters_dict)
|
||||
"""
|
||||
if not conditions:
|
||||
return "", {}
|
||||
|
||||
|
||||
if not self._validate_logical_operator(logical_operator):
|
||||
return "", {}
|
||||
|
||||
|
||||
condition_parts = []
|
||||
all_params = {}
|
||||
|
||||
|
||||
for condition_dict in conditions:
|
||||
try:
|
||||
column = condition_dict.get('column', '')
|
||||
operator = condition_dict.get('operator', '')
|
||||
value = condition_dict.get('value', '')
|
||||
|
||||
column = condition_dict.get("column", "")
|
||||
operator = condition_dict.get("operator", "")
|
||||
value = condition_dict.get("value", "")
|
||||
|
||||
# Validate each component
|
||||
if not self._validate_column_name(column):
|
||||
mylog('verbose', [f'[SafeConditionBuilder] Invalid column: {column}'])
|
||||
mylog(
|
||||
"verbose", [f"[SafeConditionBuilder] Invalid column: {column}"]
|
||||
)
|
||||
return "", {}
|
||||
|
||||
|
||||
if not self._validate_operator(operator):
|
||||
mylog('verbose', [f'[SafeConditionBuilder] Invalid operator: {operator}'])
|
||||
mylog(
|
||||
"verbose",
|
||||
[f"[SafeConditionBuilder] Invalid operator: {operator}"],
|
||||
)
|
||||
return "", {}
|
||||
|
||||
|
||||
# Create parameter binding
|
||||
param_name = self._generate_param_name()
|
||||
all_params[param_name] = self._sanitize_string(str(value))
|
||||
|
||||
|
||||
# Build condition part
|
||||
condition_part = f"{column} {operator} :{param_name}"
|
||||
condition_parts.append(condition_part)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
mylog('verbose', [f'[SafeConditionBuilder] Error processing condition: {e}'])
|
||||
mylog(
|
||||
"verbose",
|
||||
[f"[SafeConditionBuilder] Error processing condition: {e}"],
|
||||
)
|
||||
return "", {}
|
||||
|
||||
|
||||
if not condition_parts:
|
||||
return "", {}
|
||||
|
||||
|
||||
# Join all parts with the logical operator
|
||||
final_condition = f" {logical_operator} ".join(condition_parts)
|
||||
self.parameters.update(all_params)
|
||||
|
||||
|
||||
return final_condition, self.parameters
|
||||
|
||||
def build_event_type_filter(self, event_types: List[str]) -> Tuple[str, Dict[str, Any]]:
|
||||
def build_event_type_filter(
|
||||
self, event_types: List[str]
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""
|
||||
Build a safe event type filter condition.
|
||||
|
||||
|
||||
Args:
|
||||
event_types: List of event types to filter for
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (safe_sql_snippet, parameters_dict)
|
||||
"""
|
||||
@@ -586,7 +646,10 @@ class SafeConditionBuilder:
|
||||
if event_type in self.ALLOWED_EVENT_TYPES:
|
||||
valid_types.append(event_type)
|
||||
else:
|
||||
mylog('verbose', f'[SafeConditionBuilder] Invalid event type filtered out: {event_type}')
|
||||
mylog(
|
||||
"verbose",
|
||||
f"[SafeConditionBuilder] Invalid event type filtered out: {event_type}",
|
||||
)
|
||||
|
||||
if not valid_types:
|
||||
return "", {}
|
||||
@@ -594,21 +657,23 @@ class SafeConditionBuilder:
|
||||
# Generate parameters for each valid event type
|
||||
param_names = []
|
||||
for event_type in valid_types:
|
||||
param_name = self._generate_param_name('event_type')
|
||||
param_name = self._generate_param_name("event_type")
|
||||
self.parameters[param_name] = event_type
|
||||
param_names.append(f":{param_name}")
|
||||
|
||||
sql_snippet = f"AND eve_EventType IN ({', '.join(param_names)})"
|
||||
return sql_snippet, self.parameters
|
||||
|
||||
def get_safe_condition_legacy(self, condition_setting: str) -> Tuple[str, Dict[str, Any]]:
|
||||
def get_safe_condition_legacy(
|
||||
self, condition_setting: str
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""
|
||||
Convert legacy condition settings to safe parameterized queries.
|
||||
This method provides backward compatibility for existing condition formats.
|
||||
|
||||
|
||||
Args:
|
||||
condition_setting: The condition string from settings
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (safe_sql_snippet, parameters_dict)
|
||||
"""
|
||||
@@ -619,15 +684,18 @@ class SafeConditionBuilder:
|
||||
return self.build_safe_condition(condition_setting)
|
||||
except ValueError as e:
|
||||
# Log the error and return empty condition for safety
|
||||
mylog('verbose', f'[SafeConditionBuilder] Unsafe condition rejected: {condition_setting}, Error: {e}')
|
||||
mylog(
|
||||
"verbose",
|
||||
f"[SafeConditionBuilder] Unsafe condition rejected: {condition_setting}, Error: {e}",
|
||||
)
|
||||
return "", {}
|
||||
|
||||
|
||||
def create_safe_condition_builder() -> SafeConditionBuilder:
|
||||
"""
|
||||
Factory function to create a new SafeConditionBuilder instance.
|
||||
|
||||
|
||||
Returns:
|
||||
New SafeConditionBuilder instance
|
||||
"""
|
||||
return SafeConditionBuilder()
|
||||
return SafeConditionBuilder()
|
||||
|
||||
Reference in New Issue
Block a user