Move all check- scripts to /entrypoint.d/ for better organization

This commit is contained in:
Adam Outler
2025-10-29 23:49:37 +00:00
parent 0079ece1e2
commit 8cb1836777
17 changed files with 386 additions and 357 deletions

View File

@@ -0,0 +1,62 @@
#!/bin/sh
# 0-storage-permission.sh: Fix permissions if running as root.
#
# This script checks if running as root and fixes ownership and permissions
# for read-write paths to ensure proper operation.
# --- Color Codes ---
MAGENTA='\033[1;35m'
RESET='\033[0m'
# --- Main Logic ---
# Define paths that need read-write access
READ_WRITE_PATHS="
${NETALERTX_API}
${NETALERTX_LOG}
${SYSTEM_SERVICES_RUN}
${NETALERTX_CONFIG}
${NETALERTX_CONFIG_FILE}
${NETALERTX_DB}
${NETALERTX_DB_FILE}
"
# If running as root, fix permissions first
if [ "$(id -u)" -eq 0 ]; then
>&2 printf "%s" "${MAGENTA}"
>&2 cat <<'EOF'
══════════════════════════════════════════════════════════════════════════════
🚨 CRITICAL SECURITY ALERT: NetAlertX is running as ROOT (UID 0)! 🚨
This configuration bypasses all built-in security hardening measures.
You've granted a network monitoring application unrestricted access to
your host system. A successful compromise here could jeopardize your
entire infrastructure.
IMMEDIATE ACTION REQUIRED: Switch to the dedicated 'netalertx' user:
* Remove any 'user:' directive specifying UID 0 from docker-compose.yml or
* switch to the default USER in the image (20211:20211)
IMPORTANT: This corrective mode automatically adjusts ownership of
/app/db and /app/config directories to the netalertx user, ensuring
proper operation in subsequent runs.
Remember: Never operate security-critical tools as root unless you're
actively trying to get pwned.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
# Set ownership to netalertx user for all read-write paths
chown -R netalertx ${READ_WRITE_PATHS}
# Set directory and file permissions for all read-write paths
find ${READ_WRITE_PATHS} -type d -exec chmod u+rwx {} + 2>/dev/null
find ${READ_WRITE_PATHS} -type f -exec chmod u+rw {} + 2>/dev/null
echo Permissions fixed for read-write paths. Please restart the container as user 20211.
sleep infinity & wait $!; exit 211
fi

View File

@@ -0,0 +1,242 @@
#!/usr/bin/env python3
import os
import sys
from dataclasses import dataclass
@dataclass
class MountCheckResult:
"""Object to track mount status and potential issues."""
var_name: str
path: str = ""
is_writeable: bool = False
is_mounted: bool = False
is_ramdisk: bool = False
underlying_fs_is_ramdisk: bool = False # Track this separately
fstype: str = "N/A"
error: bool = False
write_error: bool = False
performance_issue: bool = False
dataloss_risk: bool = False
def get_mount_info():
"""Parses /proc/mounts to get a dict of {mount_point: fstype}."""
mounts = {}
try:
with open('/proc/mounts', 'r') as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 3:
mount_point = parts[1].replace('\\040', ' ')
fstype = parts[2]
mounts[mount_point] = fstype
except FileNotFoundError:
print("Error: /proc/mounts not found. Not a Linux system?", file=sys.stderr)
return None
return mounts
def analyze_path(var_name, is_persistent, mounted_filesystems, non_persistent_fstypes, read_only_vars):
"""
Analyzes a single path, checking for errors, performance, and dataloss.
"""
result = MountCheckResult(var_name=var_name)
target_path = os.environ.get(var_name)
if target_path is None:
result.path = f"({var_name} unset)"
result.error = True
return result
result.path = target_path
# --- 1. Check Write Permissions ---
is_writeable = os.access(target_path, os.W_OK)
if not is_writeable and not os.path.exists(target_path):
parent_dir = os.path.dirname(target_path)
if os.access(parent_dir, os.W_OK):
is_writeable = True
result.is_writeable = is_writeable
if var_name not in read_only_vars and not result.is_writeable:
result.error = True
result.write_error = True
# --- 2. Check Filesystem Type (Parent and Self) ---
parent_mount_fstype = ""
longest_mount = ""
for mount_point, fstype in mounted_filesystems.items():
if target_path.startswith(mount_point):
if len(mount_point) > len(longest_mount):
longest_mount = mount_point
parent_mount_fstype = fstype
result.underlying_fs_is_ramdisk = parent_mount_fstype in non_persistent_fstypes
if parent_mount_fstype:
result.fstype = parent_mount_fstype
# --- 3. Check if path IS a mount point ---
if target_path in mounted_filesystems:
result.is_mounted = True
result.fstype = mounted_filesystems[target_path]
result.is_ramdisk = result.fstype in non_persistent_fstypes
else:
result.is_mounted = False
result.is_ramdisk = False
# --- 4. Apply Risk Logic ---
if is_persistent:
if result.underlying_fs_is_ramdisk:
result.dataloss_risk = True
if not result.is_mounted:
result.dataloss_risk = True
else:
# Performance issue if it's not a ramdisk mount
if not result.is_mounted or not result.is_ramdisk:
result.performance_issue = True
return result
def print_warning_message():
"""Prints a formatted warning to stderr."""
YELLOW = '\033[1;33m'
RESET = '\033[0m'
message = (
"══════════════════════════════════════════════════════════════════════════════\n"
"⚠️ ATTENTION: Configuration issues detected (marked with ❌).\n\n"
" Your configuration has write permission, dataloss, or performance issues\n"
" as shown in the table above.\n\n"
" We recommend starting with the default docker-compose.yml as the\n"
" configuration can be quite complex.\n\n"
" Review the documentation for a correct setup:\n"
" https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md\n"
"══════════════════════════════════════════════════════════════════════════════\n"
)
print(f"{YELLOW}{message}{RESET}", file=sys.stderr)
def main():
NON_PERSISTENT_FSTYPES = {'tmpfs', 'ramfs'}
PERSISTENT_VARS = {'NETALERTX_DB', 'NETALERTX_CONFIG'}
# Define all possible read-only vars
READ_ONLY_VARS = {'SYSTEM_NGINX_CONFIG', 'SYSTEM_SERVICES_ACTIVE_CONFIG'}
# Base paths to check
PATHS_TO_CHECK = {
'NETALERTX_DB': True,
'NETALERTX_CONFIG': True,
'NETALERTX_API': False,
'NETALERTX_LOG': False,
'SYSTEM_SERVICES_RUN': False,
}
# *** KEY CHANGE: Conditionally add path based on PORT ***
port_val = os.environ.get("PORT")
if port_val is not None and port_val != "20211":
PATHS_TO_CHECK['SYSTEM_SERVICES_ACTIVE_CONFIG'] = False
# *** END KEY CHANGE ***
mounted_filesystems = get_mount_info()
if mounted_filesystems is None:
sys.exit(1)
results = []
has_issues = False
for var_name, is_persistent in PATHS_TO_CHECK.items():
result = analyze_path(
var_name, is_persistent,
mounted_filesystems, NON_PERSISTENT_FSTYPES, READ_ONLY_VARS
)
if result.performance_issue or result.dataloss_risk or result.error:
has_issues = True
results.append(result)
if has_issues:
# --- Print Table ---
headers = ["Path", "Writeable", "Mount", "RAMDisk", "Performance", "DataLoss"]
CHECK_SYMBOL = ""
CROSS_SYMBOL = ""
BLANK_SYMBOL = ""
bool_to_check = lambda is_good: CHECK_SYMBOL if is_good else CROSS_SYMBOL
col_widths = [len(h) for h in headers]
for r in results:
col_widths[0] = max(col_widths[0], len(str(r.path)))
header_fmt = (
f" {{:<{col_widths[0]}}} |"
f" {{:^{col_widths[1]}}} |"
f" {{:^{col_widths[2]}}} |"
f" {{:^{col_widths[3]}}} |"
f" {{:^{col_widths[4]}}} |"
f" {{:^{col_widths[5]}}} "
)
row_fmt = (
f" {{:<{col_widths[0]}}} |"
f" {{:^{col_widths[1]}}}|" # No space
f" {{:^{col_widths[2]}}}|" # No space
f" {{:^{col_widths[3]}}}|" # No space
f" {{:^{col_widths[4]}}}|" # No space
f" {{:^{col_widths[5]}}} " # DataLoss is last, needs space
)
separator = (
"-" * (col_widths[0] + 2) + "+" +
"-" * (col_widths[1] + 2) + "+" +
"-" * (col_widths[2] + 2) + "+" +
"-" * (col_widths[3] + 2) + "+" +
"-" * (col_widths[4] + 2) + "+" +
"-" * (col_widths[5] + 2)
)
print(header_fmt.format(*headers))
print(separator)
for r in results:
is_persistent = r.var_name in PERSISTENT_VARS
# --- Symbol Logic ---
write_symbol = bool_to_check(r.is_writeable)
# Special case for read-only vars
if r.var_name in READ_ONLY_VARS:
write_symbol = CHECK_SYMBOL
mount_symbol = CHECK_SYMBOL if r.is_mounted else CROSS_SYMBOL
ramdisk_symbol = ""
if is_persistent:
ramdisk_symbol = CROSS_SYMBOL if r.underlying_fs_is_ramdisk else BLANK_SYMBOL
else:
ramdisk_symbol = CHECK_SYMBOL if r.is_ramdisk else CROSS_SYMBOL
if is_persistent:
perf_symbol = BLANK_SYMBOL
else:
perf_symbol = bool_to_check(not r.performance_issue)
dataloss_symbol = bool_to_check(not r.dataloss_risk)
print(row_fmt.format(
r.path,
write_symbol,
mount_symbol,
ramdisk_symbol,
perf_symbol,
dataloss_symbol
))
# --- Print Warning ---
print("\n", file=sys.stderr)
print_warning_message()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,26 @@
#!/bin/sh
# first-run-check.sh - Checks and initializes configuration files on first run
# Check for app.conf and deploy if required
if [ ! -f ${NETALERTX_CONFIG}/app.conf ]; then
mkdir -p "${NETALERTX_CONFIG}" || {
>&2 echo "ERROR: Failed to create config directory ${NETALERTX_CONFIG}"
exit 1
}
cp /app/back/app.conf "${NETALERTX_CONFIG}/app.conf" || {
>&2 echo "ERROR: Failed to copy default config to ${NETALERTX_CONFIG}/app.conf"
exit 2
}
RESET='\033[0m'
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
🆕 First run detected. Default configuration written to ${NETALERTX_CONFIG}/app.conf.
Review your settings in the UI or edit the file directly before trusting
this instance in production.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
fi

View File

@@ -0,0 +1,464 @@
#!/bin/sh
# This script checks if the database file exists, and if not, creates it with the initial schema.
# It is intended to be run at the first start of the application.
# If ALWAYS_FRESH_INSTALL is true, remove the database to force a rebuild.
if [ "${ALWAYS_FRESH_INSTALL}" = "true" ]; then
if [ -f "${NETALERTX_DB_FILE}" ]; then
# Provide feedback to the user.
>&2 echo "INFO: ALWAYS_FRESH_INSTALL is true. Removing existing database to force a fresh installation."
rm -f "${NETALERTX_DB_FILE}" "${NETALERTX_DB_FILE}-shm" "${NETALERTX_DB_FILE}-wal"
fi
# Otherwise, if the db exists, exit.
elif [ -f "${NETALERTX_DB_FILE}" ]; then
exit 0
fi
CYAN='\033[1;36m'
RESET='\033[0m'
>&2 printf "%s" "${CYAN}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
🆕 First run detected. Building initial database schema in ${NETALERTX_DB_FILE}.
Do not interrupt this step. Once complete, consider backing up the fresh
database before onboarding sensitive networks.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
# Write all text to db file until we see "end-of-database-schema"
sqlite3 "${NETALERTX_DB_FILE}" <<'end-of-database-schema'
CREATE TABLE Events (eve_MAC STRING (50) NOT NULL COLLATE NOCASE, eve_IP STRING (50) NOT NULL COLLATE NOCASE, eve_DateTime DATETIME NOT NULL, eve_EventType STRING (30) NOT NULL COLLATE NOCASE, eve_AdditionalInfo STRING (250) DEFAULT (''), eve_PendingAlertEmail BOOLEAN NOT NULL CHECK (eve_PendingAlertEmail IN (0, 1)) DEFAULT (1), eve_PairEventRowid INTEGER);
CREATE TABLE Sessions (ses_MAC STRING (50) COLLATE NOCASE, ses_IP STRING (50) COLLATE NOCASE, ses_EventTypeConnection STRING (30) COLLATE NOCASE, ses_DateTimeConnection DATETIME, ses_EventTypeDisconnection STRING (30) COLLATE NOCASE, ses_DateTimeDisconnection DATETIME, ses_StillConnected BOOLEAN, ses_AdditionalInfo STRING (250));
CREATE TABLE IF NOT EXISTS "Online_History" (
"Index" INTEGER,
"Scan_Date" TEXT,
"Online_Devices" INTEGER,
"Down_Devices" INTEGER,
"All_Devices" INTEGER,
"Archived_Devices" INTEGER,
"Offline_Devices" INTEGER,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE Devices (
devMac STRING (50) PRIMARY KEY NOT NULL COLLATE NOCASE,
devName STRING (50) NOT NULL DEFAULT "(unknown)",
devOwner STRING (30) DEFAULT "(unknown)" NOT NULL,
devType STRING (30),
devVendor STRING (250),
devFavorite BOOLEAN CHECK (devFavorite IN (0, 1)) DEFAULT (0) NOT NULL,
devGroup STRING (10),
devComments TEXT,
devFirstConnection DATETIME NOT NULL,
devLastConnection DATETIME NOT NULL,
devLastIP STRING (50) NOT NULL COLLATE NOCASE,
devStaticIP BOOLEAN DEFAULT (0) NOT NULL CHECK (devStaticIP IN (0, 1)),
devScan INTEGER DEFAULT (1) NOT NULL,
devLogEvents BOOLEAN NOT NULL DEFAULT (1) CHECK (devLogEvents IN (0, 1)),
devAlertEvents BOOLEAN NOT NULL DEFAULT (1) CHECK (devAlertEvents IN (0, 1)),
devAlertDown BOOLEAN NOT NULL DEFAULT (0) CHECK (devAlertDown IN (0, 1)),
devSkipRepeated INTEGER DEFAULT 0 NOT NULL,
devLastNotification DATETIME,
devPresentLastScan BOOLEAN NOT NULL DEFAULT (0) CHECK (devPresentLastScan IN (0, 1)),
devIsNew BOOLEAN NOT NULL DEFAULT (1) CHECK (devIsNew IN (0, 1)),
devLocation STRING (250) COLLATE NOCASE,
devIsArchived BOOLEAN NOT NULL DEFAULT (0) CHECK (devIsArchived IN (0, 1)),
devParentMAC TEXT,
devParentPort INTEGER,
devIcon TEXT,
devGUID TEXT,
devSite TEXT,
devSSID TEXT,
devSyncHubNode TEXT,
devSourcePlugin TEXT
, "devCustomProps" TEXT);
CREATE TABLE IF NOT EXISTS "Settings" (
"setKey" TEXT,
"setName" TEXT,
"setDescription" TEXT,
"setType" TEXT,
"setOptions" TEXT,
"setGroup" TEXT,
"setValue" TEXT,
"setEvents" TEXT,
"setOverriddenByEnv" INTEGER
);
CREATE TABLE IF NOT EXISTS "Parameters" (
"par_ID" TEXT PRIMARY KEY,
"par_Value" TEXT
);
CREATE TABLE 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)
);
CREATE TABLE 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, "ObjectGUID" TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE 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, "ObjectGUID" TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE 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)
);
CREATE TABLE 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),
UNIQUE(cur_MAC)
);
CREATE TABLE IF NOT EXISTS "AppEvents" (
"Index" INTEGER PRIMARY KEY AUTOINCREMENT,
"GUID" TEXT UNIQUE,
"AppEventProcessed" BOOLEAN,
"DateTimeCreated" TEXT,
"ObjectType" TEXT,
"ObjectGUID" TEXT,
"ObjectPlugin" TEXT,
"ObjectPrimaryID" TEXT,
"ObjectSecondaryID" TEXT,
"ObjectForeignKey" TEXT,
"ObjectIndex" TEXT,
"ObjectIsNew" BOOLEAN,
"ObjectIsArchived" BOOLEAN,
"ObjectStatusColumn" TEXT,
"ObjectStatus" TEXT,
"AppEventType" TEXT,
"Helper1" TEXT,
"Helper2" TEXT,
"Helper3" TEXT,
"Extra" TEXT
);
CREATE TABLE IF NOT EXISTS "Notifications" (
"Index" INTEGER,
"GUID" TEXT UNIQUE,
"DateTimeCreated" TEXT,
"DateTimePushed" TEXT,
"Status" TEXT,
"JSON" TEXT,
"Text" TEXT,
"HTML" TEXT,
"PublishedVia" TEXT,
"Extra" TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE INDEX IDX_eve_DateTime ON Events (eve_DateTime);
CREATE INDEX IDX_eve_EventType ON Events (eve_EventType COLLATE NOCASE);
CREATE INDEX IDX_eve_MAC ON Events (eve_MAC COLLATE NOCASE);
CREATE INDEX IDX_eve_PairEventRowid ON Events (eve_PairEventRowid);
CREATE INDEX IDX_ses_EventTypeDisconnection ON Sessions (ses_EventTypeDisconnection COLLATE NOCASE);
CREATE INDEX IDX_ses_EventTypeConnection ON Sessions (ses_EventTypeConnection COLLATE NOCASE);
CREATE INDEX IDX_ses_DateTimeDisconnection ON Sessions (ses_DateTimeDisconnection);
CREATE INDEX IDX_ses_MAC ON Sessions (ses_MAC COLLATE NOCASE);
CREATE INDEX IDX_ses_DateTimeConnection ON Sessions (ses_DateTimeConnection);
CREATE INDEX IDX_dev_PresentLastScan ON Devices (devPresentLastScan);
CREATE INDEX IDX_dev_FirstConnection ON Devices (devFirstConnection);
CREATE INDEX IDX_dev_AlertDeviceDown ON Devices (devAlertDown);
CREATE INDEX IDX_dev_StaticIP ON Devices (devStaticIP);
CREATE INDEX IDX_dev_ScanCycle ON Devices (devScan);
CREATE INDEX IDX_dev_Favorite ON Devices (devFavorite);
CREATE INDEX IDX_dev_LastIP ON Devices (devLastIP);
CREATE INDEX IDX_dev_NewDevice ON Devices (devIsNew);
CREATE INDEX IDX_dev_Archived ON Devices (devIsArchived);
CREATE VIEW Events_Devices AS
SELECT *
FROM Events
LEFT JOIN Devices ON eve_MAC = devMac
/* Events_Devices(eve_MAC,eve_IP,eve_DateTime,eve_EventType,eve_AdditionalInfo,eve_PendingAlertEmail,eve_PairEventRowid,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) */;
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
/* LatestEventsPerMAC(eve_MAC,eve_IP,eve_DateTime,eve_EventType,eve_AdditionalInfo,eve_PendingAlertEmail,eve_PairEventRowid,row_num,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,cur_MAC,cur_IP,cur_Vendor,cur_ScanMethod,cur_Name,cur_LastQuery,cur_DateTime,cur_SyncHubNodeName,cur_NetworkSite,cur_SSID,cur_NetworkNodeMAC,cur_PORT,cur_Type) */;
CREATE VIEW Sessions_Devices AS SELECT * FROM Sessions LEFT JOIN "Devices" ON ses_MAC = devMac
/* Sessions_Devices(ses_MAC,ses_IP,ses_EventTypeConnection,ses_DateTimeConnection,ses_EventTypeDisconnection,ses_DateTimeDisconnection,ses_StillConnected,ses_AdditionalInfo,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) */;
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
/* Convert_Events_to_Sessions(eve_MAC,eve_IP,eve_EventTypeConnection,eve_DateTimeConnection,eve_EventTypeDisconnection,eve_DateTimeDisconnection,eve_StillConnected,eve_AdditionalInfo) */;
CREATE TRIGGER "trg_insert_devices"
AFTER INSERT ON "Devices"
WHEN NOT EXISTS (
SELECT 1 FROM AppEvents
WHERE AppEventProcessed = 0
AND ObjectType = 'Devices'
AND ObjectGUID = NEW.devGUID
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
AND AppEventType = 'insert'
)
BEGIN
INSERT INTO "AppEvents" (
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPrimaryID",
"ObjectSecondaryID",
"ObjectStatus",
"ObjectStatusColumn",
"ObjectIsNew",
"ObjectIsArchived",
"ObjectForeignKey",
"ObjectPlugin",
"AppEventType"
)
VALUES (
lower(
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
substr(hex( randomblob(2)), 2) || '-' ||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
substr(hex(randomblob(2)), 2) || '-' ||
hex(randomblob(6))
)
,
DATETIME('now'),
FALSE,
'Devices',
NEW.devGUID, -- ObjectGUID
NEW.devMac, -- ObjectPrimaryID
NEW.devLastIP, -- ObjectSecondaryID
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus
'devPresentLastScan', -- ObjectStatusColumn
NEW.devIsNew, -- ObjectIsNew
NEW.devIsArchived, -- ObjectIsArchived
NEW.devGUID, -- ObjectForeignKey
'DEVICES', -- ObjectForeignKey
'insert'
);
END;
CREATE TRIGGER "trg_update_devices"
AFTER UPDATE ON "Devices"
WHEN NOT EXISTS (
SELECT 1 FROM AppEvents
WHERE AppEventProcessed = 0
AND ObjectType = 'Devices'
AND ObjectGUID = NEW.devGUID
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
AND AppEventType = 'update'
)
BEGIN
INSERT INTO "AppEvents" (
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPrimaryID",
"ObjectSecondaryID",
"ObjectStatus",
"ObjectStatusColumn",
"ObjectIsNew",
"ObjectIsArchived",
"ObjectForeignKey",
"ObjectPlugin",
"AppEventType"
)
VALUES (
lower(
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
substr(hex( randomblob(2)), 2) || '-' ||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
substr(hex(randomblob(2)), 2) || '-' ||
hex(randomblob(6))
)
,
DATETIME('now'),
FALSE,
'Devices',
NEW.devGUID, -- ObjectGUID
NEW.devMac, -- ObjectPrimaryID
NEW.devLastIP, -- ObjectSecondaryID
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus
'devPresentLastScan', -- ObjectStatusColumn
NEW.devIsNew, -- ObjectIsNew
NEW.devIsArchived, -- ObjectIsArchived
NEW.devGUID, -- ObjectForeignKey
'DEVICES', -- ObjectForeignKey
'update'
);
END;
CREATE TRIGGER "trg_delete_devices"
AFTER DELETE ON "Devices"
WHEN NOT EXISTS (
SELECT 1 FROM AppEvents
WHERE AppEventProcessed = 0
AND ObjectType = 'Devices'
AND ObjectGUID = OLD.devGUID
AND ObjectStatus = CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
AND AppEventType = 'delete'
)
BEGIN
INSERT INTO "AppEvents" (
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPrimaryID",
"ObjectSecondaryID",
"ObjectStatus",
"ObjectStatusColumn",
"ObjectIsNew",
"ObjectIsArchived",
"ObjectForeignKey",
"ObjectPlugin",
"AppEventType"
)
VALUES (
lower(
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
substr(hex( randomblob(2)), 2) || '-' ||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
substr(hex(randomblob(2)), 2) || '-' ||
hex(randomblob(6))
)
,
DATETIME('now'),
FALSE,
'Devices',
OLD.devGUID, -- ObjectGUID
OLD.devMac, -- ObjectPrimaryID
OLD.devLastIP, -- ObjectSecondaryID
CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus
'devPresentLastScan', -- ObjectStatusColumn
OLD.devIsNew, -- ObjectIsNew
OLD.devIsArchived, -- ObjectIsArchived
OLD.devGUID, -- ObjectForeignKey
'DEVICES', -- ObjectForeignKey
'delete'
);
END;
end-of-database-schema
if [ $? -ne 0 ]; then
RED='\033[1;31m'
RESET='\033[0m'
>&2 printf "%s" "${RED}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
❌ CRITICAL: Database schema creation failed for ${NETALERTX_DB_FILE}.
NetAlertX cannot start without a properly initialized database. This
failure typically indicates:
* Insufficient disk space or write permissions in the database directory
* Corrupted or inaccessible SQLite installation
* File system issues preventing database file creation
Check the logs for detailed SQLite error messages. Ensure the container
has write access to the database path and adequate storage space.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 1
fi

View File

@@ -0,0 +1,53 @@
#!/bin/sh
# Initialize required directories and log files
# These must exist before services start to avoid permission/write errors
check_mandatory_folders() {
# Check and create plugins log directory
if [ ! -d "${NETALERTX_PLUGINS_LOG}" ]; then
echo " * Creating Plugins log."
if ! mkdir -p "${NETALERTX_PLUGINS_LOG}"; then
echo "Error: Failed to create plugins log directory: ${NETALERTX_PLUGINS_LOG}"
return 1
fi
fi
# Check and create system services run log directory
if [ ! -d "${SYSTEM_SERVICES_RUN_LOG}" ]; then
echo " * Creating System services run log."
if ! mkdir -p "${SYSTEM_SERVICES_RUN_LOG}"; then
echo "Error: Failed to create system services run log directory: ${SYSTEM_SERVICES_RUN_LOG}"
return 1
fi
fi
# Check and create system services run tmp directory
if [ ! -d "${SYSTEM_SERVICES_RUN_TMP}" ]; then
echo " * Creating System services run tmp."
if ! mkdir -p "${SYSTEM_SERVICES_RUN_TMP}"; then
echo "Error: Failed to create system services run tmp directory: ${SYSTEM_SERVICES_RUN_TMP}"
return 1
fi
fi
# Check and create DB locked log file
if [ ! -f "${LOG_DB_IS_LOCKED}" ]; then
echo " * Creating DB locked log."
if ! touch "${LOG_DB_IS_LOCKED}"; then
echo "Error: Failed to create DB locked log file: ${LOG_DB_IS_LOCKED}"
return 1
fi
fi
# Check and create execution queue log file
if [ ! -f "${LOG_EXECUTION_QUEUE}" ]; then
echo " * Creating Execution queue log."
if ! touch "${LOG_EXECUTION_QUEUE}"; then
echo "Error: Failed to create execution queue log file: ${LOG_EXECUTION_QUEUE}"
return 1
fi
fi
}
# Run the function
check_mandatory_folders

View File

@@ -0,0 +1,72 @@
#!/bin/sh
# 30-writable-config.sh: Verify read/write permissions for config and database files.
#
# This script ensures that the application can read from and write to the
# critical configuration and database files after startup.
# --- Color Codes ---
RED='\033[1;31m'
YELLOW='\033[1;33m'
RESET='\033[0m'
# --- Main Logic ---
# Define paths that need read-write access
READ_WRITE_PATHS="
${NETALERTX_CONFIG_FILE}
${NETALERTX_DB_FILE}
"
# --- Permission Validation ---
failures=0
# Check read-write paths for existence, read, and write access
for path in $READ_WRITE_PATHS; do
if [ ! -e "$path" ]; then
failures=1
>&2 printf "%s" "${RED}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
❌ CRITICAL: Path does not exist.
The required path "${path}" could not be found. The application
cannot start without its complete directory structure.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
elif [ ! -r "$path" ]; then
failures=1
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Read permission denied.
The application cannot read from "${path}". This will cause
unpredictable errors. Please correct the file system permissions.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
elif [ ! -w "$path" ]; then
failures=1
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Write permission denied.
The application cannot write to "${path}". This will prevent it from
saving data, logs, or configuration.
To fix this automatically, restart the container with root privileges
(e.g., remove the "user:" directive in your Docker Compose file).
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
fi
done
# If there were any failures, exit
if [ "$failures" -ne 0 ]; then
exit 1
fi

View File

@@ -0,0 +1,50 @@
#!/bin/sh
# check-nginx-config.sh - verify nginx conf.active mount is writable when startup needs to render config.
CONF_ACTIVE_DIR="${SYSTEM_SERVICES_ACTIVE_CONFIG}"
TARGET_FILE="${CONF_ACTIVE_DIR}/netalertx.conf"
# If the directory is missing entirely we warn and exit failure so the caller can see the message.
if [ ! -d "${CONF_ACTIVE_DIR}" ]; then
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Nginx configuration mount ${CONF_ACTIVE_DIR} is missing.
Custom listen address or port changes require a writable nginx conf.active
directory. Without it, the container falls back to defaults and ignores
your overrides.
Create a bind mount:
--mount type=bind,src=/path/on/host,dst=${CONF_ACTIVE_DIR}
and ensure it is owned by the netalertx user (20211:20211) with 700 perms.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 1
fi
TMP_FILE="${CONF_ACTIVE_DIR}/.netalertx-write-test"
if ! ( : >"${TMP_FILE}" ) 2>/dev/null; then
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Unable to write to ${TARGET_FILE}.
Ensure the conf.active mount is writable by the netalertx user before
changing LISTEN_ADDR or PORT. Fix permissions:
chown -R 20211:20211 ${CONF_ACTIVE_DIR}
find ${CONF_ACTIVE_DIR} -type d -exec chmod 700 {} +
find ${CONF_ACTIVE_DIR} -type f -exec chmod 600 {} +
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 1
fi
rm -f "${TMP_FILE}"
exit 0

View File

@@ -0,0 +1,41 @@
#!/bin/sh
# check-user-netalertx.sh - ensure the container is running as the hardened service user.
EXPECTED_USER="${NETALERTX_USER:-netalertx}"
EXPECTED_UID="$(getent passwd "${EXPECTED_USER}" 2>/dev/null | cut -d: -f3)"
EXPECTED_GID="$(getent passwd "${EXPECTED_USER}" 2>/dev/null | cut -d: -f4)"
CURRENT_UID="$(id -u)"
CURRENT_GID="$(id -g)"
# Fallback to known defaults when lookups fail
if [ -z "${EXPECTED_UID}" ]; then
EXPECTED_UID="20211"
fi
if [ -z "${EXPECTED_GID}" ]; then
EXPECTED_GID="20211"
fi
if [ "${CURRENT_UID}" -eq "${EXPECTED_UID}" ] && [ "${CURRENT_GID}" -eq "${EXPECTED_GID}" ]; then
exit 0
fi
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: NetAlertX is running as UID ${CURRENT_UID}:${CURRENT_GID}.
Hardened permissions, file ownership, and runtime isolation expect the
dedicated service account (${EXPECTED_USER} -> ${EXPECTED_UID}:${EXPECTED_GID}).
When you override the container user (for example, docker run --user 1000:1000
or a Compose "user:" directive), NetAlertX loses crucial safeguards and
future upgrades may silently fail.
Restore the container to the default user:
* Remove any custom --user flag
* Delete "user:" overrides in compose files
* Recreate the container so volume ownership is reset
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"

View File

@@ -0,0 +1,64 @@
#!/bin/sh
# detect when the container is not using host networking.
# Exit if NETALERTX_DEBUG=1
if [ "${NETALERTX_DEBUG}" = "1" ]; then
exit 0
fi
# Get the default network interface
DEFAULT_IF="$(ip route show default 0.0.0.0/0 2>/dev/null | awk 'NR==1 {print $5}')"
if [ -z "${DEFAULT_IF}" ]; then
# No default route; nothing to validate.
exit 0
fi
IF_LINK_INFO="$(ip link show "${DEFAULT_IF}" 2>/dev/null)"
IF_IP="$(ip -4 addr show "${DEFAULT_IF}" 2>/dev/null | awk '/inet / {print $2}' | head -n1)"
IF_MAC=""
if [ -r "/sys/class/net/${DEFAULT_IF}/address" ]; then
IF_MAC="$(cat "/sys/class/net/${DEFAULT_IF}/address")"
fi
looks_like_bridge="0"
# Check for common bridge MAC and IP patterns
case "${IF_MAC}" in
02:42:*) looks_like_bridge="1" ;;
00:00:00:00:00:00) looks_like_bridge="1" ;;
"") ;; # leave as is
esac
# Check for common bridge IP ranges
case "${IF_IP}" in
172.1[6-9].*|172.2[0-9].*|172.3[0-1].*) looks_like_bridge="1" ;;
192.168.65.*) looks_like_bridge="1" ;;
esac
if echo "${IF_LINK_INFO}" | grep -q "@if"; then
looks_like_bridge="1"
fi
if [ "${looks_like_bridge}" -ne 1 ]; then
exit 0
fi
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: NetAlertX is not running with --network=host.
Bridge networking blocks passive discovery (ARP, NBNS, mDNS) and active
scanning accuracy. Most plugins expect raw access to the LAN through host
networking and CAP_NET_RAW capabilities.
Restart the container with:
docker run --network=host --cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=NET_BIND_SERVICE
or set "network_mode: host" in docker-compose.yml.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 0

View File

@@ -0,0 +1,31 @@
#!/bin/sh
# layer-2-network.sh - Uses a real nmap command to detect missing container
# privileges and warns the user. It is silent on success.
# Run a fast nmap command that requires raw sockets, capturing only stderr.
ERROR_OUTPUT=$(nmap --privileged -sS -p 20211 127.0.0.1 2>&1)
EXIT_CODE=$?
# Flag common capability errors regardless of exact exit code.
if [ "$EXIT_CODE" -ne 0 ] && \
echo "$ERROR_OUTPUT" | grep -q -e "Operation not permitted" -e "requires root privileges"
then
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<'EOF'
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Raw network capabilities are missing.
Tools that rely on NET_RAW/NET_ADMIN/NET_BIND_SERVICE (e.g. nmap -sS,
arp-scan, nbtscan) will not function. Restart the container with:
--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=NET_BIND_SERVICE
Without those caps, NetAlertX cannot inspect your network. Fix it before
trusting any results.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
fi
exit 0 # Always exit success even after warnings