This commit is contained in:
jokob-sk
2025-10-05 08:09:03 +11:00
8 changed files with 1239 additions and 23 deletions

View File

@@ -957,7 +957,7 @@ class SpeedtestResults(object):
self.client = client or {}
self._share = None
self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
self.timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z')
self.bytes_received = 0
self.bytes_sent = 0

View File

@@ -65,11 +65,11 @@ wget https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/debian12/
### Installation via curl
```bash
curl -o install.ubuntu24.sh https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/ubuntu24/install.ubuntu24.sh && sudo chmod +x install.ubuntu24.sh && sudo ./install.ubuntu24.sh
curl -o install.sh https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/ubuntu24/install.sh && sudo chmod +x install.sh && sudo ./install.sh
```
### Installation via wget
```bash
wget https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/ubuntu24/install.ubuntu24.sh -O install.ubuntu24.sh && sudo chmod +x install.ubuntu24.sh && sudo ./install.ubuntu24.sh
wget https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/ubuntu24/install.sh -O install.sh && sudo chmod +x install.sh && sudo ./install.sh
```

195
install/proxmox/README.md Normal file
View File

@@ -0,0 +1,195 @@
# NetAlertX Proxmox Installer
An installer script for deploying NetAlertX on Proxmox VE (Debian-based) systems. This installer automates the complete setup including dependencies, NGINX configuration, systemd service, and security hardening.
## 🚀 Quick Start
### Prerequisites
- Fresh LXC or VM of Debian 13 or Ubuntu 24
- Root access
- Internet connection
### Installation
```bash
# Download and run the installer
wget https://raw.githubusercontent.com/jokob-sk/NetAlertX/refs/heads/main/install/proxmox/proxmox-install-netalertx.sh -O proxmox-install-netalertx.sh && chmod +x proxmox-install-netalertx.sh && ./proxmox-install-netalertx.sh
```
## 📋 What This Installer Does
### System Dependencies
- **PHP 8.4** with FPM, SQLite3, cURL extensions
- **NGINX** with custom configuration
- **Python 3** with virtual environment
- **Network tools**: nmap, arp-scan, traceroute, mtr, speedtest-cli
- **Additional tools**: git, build-essential, avahi-daemon
### Security Features
- **Hardened permissions**: Proper user/group ownership
- **TMPFS mounts**: Log and API directories mounted as tmpfs for security
### Service Management
- **Systemd service**: Auto-start on boot with restart policies
- **Service monitoring**: Built-in health checks and logging
- **Dependency management**: Waits for network and NGINX
## 🔧 Configuration
### Port Configuration
The installer will prompt for a custom port, or defaultto 20211 after 10-seconds:
```
Enter HTTP port for NetAlertX [20211] (auto-continue in 10s):
```
### Service Management
```bash
# Check service status
systemctl status netalertx
# View logs
journalctl -u netalertx -f
# Restart service
systemctl restart netalertx
# Stop service
systemctl stop netalertx
```
## 🌐 Access
After installation, access NetAlertX at:
```
http://[SERVER_IP]:[PORT]
```
## 🔒 Security Considerations
### TMPFS Mounts
- `/app/log` - Mounted as tmpfs (no persistent logs)
- `/app/api` - Mounted as tmpfs (temporary API data)
### File Permissions
- Application files: `www-data:www-data` with appropriate permissions
- NGINX runs as `www-data` user
- Log directories: Secure permissions with tmpfs
### Network Security
- NGINX configured for internal network access
- No external firewall rules added (configure manually if needed)
## 🛠️ Troubleshooting
### Common Issues
#### 403 Forbidden Error
```bash
# Check file permissions
ls -la /var/www/html/netalertx
ls -la /app/front
# Fix permissions
chown -R www-data:www-data /app/front
chmod -R 755 /app/front
```
#### Service Won't Start
```bash
# Check service status
systemctl status netalertx
# View detailed logs
journalctl -u netalertx --no-pager -l
# Check if port is in use
ss -tlnp | grep :20211
```
#### GraphQL Connection Issues
```bash
# Check API token in config
grep API_TOKEN /app/config/app.conf
# Verify GraphQL port
grep GRAPHQL_PORT /app/config/app.conf
# Check backend logs
tail -f /app/log/app.log
```
### Log Locations
- **Service logs**: `journalctl -u netalertx`
- **Application logs**: `/app/log/` (tmpfs)
- **NGINX logs**: `/var/log/nginx/`
- **PHP logs**: `/app/log/app.php_errors.log`
### Manual Service Start
If systemd service fails:
```bash
# Activate Python environment
source /opt/myenv/bin/activate
# Start manually
cd /app
python server/
```
or
```
./start.netalertx.sh
```
## 🔄 Updates
### Updating NetAlertX
```bash
# Stop service
systemctl stop netalertx
# Update from repository
cd /app
git pull origin main
# Restart service
systemctl start netalertx
```
## 📁 File Structure
```
/app/ # Main application directory
├── front/ # Web interface (symlinked to /var/www/html/netalertx)
├── server/ # Python backend
├── config/ # Configuration files
├── db/ # Database files
├── log/ # Log files (tmpfs)
├── api/ # API files (tmpfs)
└── start.netalertx.sh # Service startup script
/etc/systemd/system/
└── netalertx.service # Systemd service definition
/etc/nginx/conf.d/
└── netalertx.conf # NGINX configuration
```
## 🤝 Contributing
This installer will need a maintainer
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## 🙏 Acknowledgments
- NetAlertX development team
- Proxmox VE community
- Debian/Ubuntu maintainers
- Open source contributors
---
**Note**: This installer was designed for a Proxmox LXC Debian 13 or Ubuntu 24 containers. For other systems, please use the appropriate installer or manual installation instructions.

View File

@@ -0,0 +1,33 @@
server {
listen 20211;
server_name _; #change this to your custom domain if you have one
# Web-interface files location
root /var/www/html/netalertx;
# Main page
index index.php;
#rewrite /app/(.*) / permanent;
add_header X-Forwarded-Prefix "/netalertx" always;
proxy_set_header X-Forwarded-Prefix "/netalertx";
# Specify a character set
charset utf-8;
location / {
# Try to serve files directly, fallback to index.php
try_files $uri $uri/ /index.php?$query_string;
}
# FastCGI configuration for PHP
location ~ \.php$ {
# Use a Unix socket for better performance
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param QUERY_STRING $query_string;
include fastcgi_params;
}
}

View File

@@ -0,0 +1,425 @@
#!/usr/bin/env bash
# Exit immediately if a command exits with a non-zero status.
set -e
# Treat unset variables as an error when substituting
set -u
# Consider failures in a pipeline
set -o pipefail
# Safe IFS
IFS=$' \t\n'
# 🛑 Important: This is only used for the bare-metal install 🛑
# Colors (guarded)
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
RESET='\e[0m'
GREEN='\e[1;38;5;2m'
RED='\e[31m'
else
RESET=''; GREEN=''; RED=''
fi
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[UPDATING] ${RESET}Making sure the system is up to date"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Running proxmox-install-netalertx.sh"
printf "%b\n" "--------------------------------------------------------------------------"
# Set environment variables
INSTALL_DIR=/app # default installation directory
# DO NOT CHANGE ANYTHING BELOW THIS LINE!
INSTALLER_DIR="$INSTALL_DIR/install/proxmox"
CONF_FILE=app.conf
DB_FILE=app.db
NGINX_CONF_FILE=netalertx.conf
WEB_UI_DIR=/var/www/html/netalertx
NGINX_CONFIG=/etc/nginx/conf.d/$NGINX_CONF_FILE
OUI_FILE="/usr/share/arp-scan/ieee-oui.txt"
FILEDB=$INSTALL_DIR/db/$DB_FILE
# DO NOT CHANGE ANYTHING ABOVE THIS LINE!
# Check if script is run as root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root."
exit 1
fi
# Interactive confirmation: warn about overwriting/removing existing installation and NGINX config
if [ -z "${NETALERTX_ASSUME_YES:-}" ] && [ -z "${ASSUME_YES:-}" ] && [ -z "${NETALERTX_FORCE:-}" ]; then
printf "%b\n" "------------------------------------------------------------------------"
printf "%b\n" "${RED}[WARNING] ${RESET}This script should be run on a fresh server"
printf "%b\n" "${RED}[WARNING] ${RESET}This script will install NetAlertX and will:"
printf "%b\n" "${RED}[WARNING] ${RESET}• Update OS with apt-get update/upgrade"
printf "%b\n" "${RED}[WARNING] ${RESET}• Overwrite existing files under ${INSTALL_DIR} "
printf "%b\n" "${RED}[WARNING] ${RESET}• Wipe any existing database"
printf "%b\n" "${RED}[WARNING] ${RESET}• Wipe/Set up NGINX configuration under /etc/nginx"
printf "%b\n" "${RED}[WARNING] ${RESET}• Set up systemd services."
read -r -p "Proceed with installation? [y/N]: " _reply
case "${_reply}" in
y|Y|yes|YES) ;;
*) echo "Aborting by user choice."; exit 1;;
esac
else
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Non-interactive mode detected; proceeding without confirmation."
printf "%b\n" "--------------------------------------------------------------------------"
fi
# Getting up to date
apt-get update -y
apt-get upgrade -y
# Prompt for HTTP port (default 20211) with countdown fallback
DEFAULT_PORT=20211
if [ -z "${NETALERTX_ASSUME_YES:-}" ] && [ -z "${ASSUME_YES:-}" ] && [ -z "${NETALERTX_FORCE:-}" ]; then
printf "%b\n" "--------------------------------------------------------------------------"
# Countdown-based prompt
_entered_port=""
for _sec in 10 9 8 7 6 5 4 3 2 1; do
printf "\rEnter HTTP port for NetAlertX [${DEFAULT_PORT}] (auto-continue in %2ds): " "${_sec}"
if read -t 1 -r _entered_port; then
break
fi
done
printf "\n"
if [ -z "${_entered_port}" ]; then
PORT="${DEFAULT_PORT}"
elif printf '%s' "${_entered_port}" | grep -Eq '^[0-9]+$' && [ "${_entered_port}" -ge 1 ] && [ "${_entered_port}" -le 65535 ]; then
PORT="${_entered_port}"
else
printf "%b\n" "${RED}[WARNING] ${RESET}Invalid port. Falling back to ${DEFAULT_PORT}"
PORT="${DEFAULT_PORT}"
fi
else
PORT="${PORT-}"; PORT="${PORT:-${DEFAULT_PORT}}"
fi
export PORT
# Detect primary server IP
SERVER_IP="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')"
if [ -z "${SERVER_IP}" ]; then
SERVER_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
fi
if [ -z "${SERVER_IP}" ]; then
SERVER_IP="127.0.0.1"
fi
export SERVER_IP
# Ensure tmpfs mounts are cleaned up on exit/failure
trap 'umount "${INSTALL_DIR}/log" 2>/dev/null || true; umount "${INSTALL_DIR}/api" 2>/dev/null || true' EXIT
# Making sure the system is clean
if [ -d "$INSTALL_DIR" ]; then
printf "%b\n" "Removing existing directory: $INSTALL_DIR"
rm -rf "$INSTALL_DIR"
fi
# 1. INSTALL SYSTEM DEPENDENCIES & ADD PHP REPOSITORY
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Installing system dependencies"
printf "%b\n" "--------------------------------------------------------------------------"
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
# software-properties-common is not available and not needed
apt-get install -y --no-install-recommends \
ca-certificates lsb-release curl gnupg
# Detect OS
. /etc/os-release
OS_ID="${ID:-}"
OS_VER="${VERSION_ID:-}"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Detected OS: ${OS_ID} ${OS_VER}"
printf "%b\n" "--------------------------------------------------------------------------"
if
[ "${OS_ID}" = "ubuntu" ] && printf '%s' "${OS_VER}" | grep -q '^24'; then
# Ubuntu 24.x typically ships PHP 8.3; add ondrej/php PPA and set 8.4
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Ubuntu 24 detected - enabling ondrej/php PPA for PHP 8.4"
printf "%b\n" "--------------------------------------------------------------------------"
apt-get install -y --no-install-recommends software-properties-common || true
add-apt-repository ppa:ondrej/php -y
apt update -y
elif
[ "${OS_ID}" = "debian" ] && printf '%s' "${OS_VER}" | grep -q '^13'; then
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Debian 13 detected - using built-in PHP 8.4"
printf "%b\n" "--------------------------------------------------------------------------"
fi
apt-get install -y --no-install-recommends \
tini snmp ca-certificates curl libwww-perl arp-scan perl apt-utils cron sudo \
php8.4 php8.4-cgi php8.4-fpm php8.4-sqlite3 php8.4-curl sqlite3 dnsutils net-tools mtr \
python3 python3-dev iproute2 nmap python3-pip zip usbutils traceroute nbtscan \
avahi-daemon avahi-utils build-essential git gnupg2 lsb-release \
debian-archive-keyring python3-venv
if
[ "${OS_ID}" = "ubuntu" ] && printf '%s' "${OS_VER}" | grep -q '^24'; then # Set PHP 8.4 as the default alternatives where applicable
update-alternatives --set php /usr/bin/php8.4 || true
systemctl enable php8.4-fpm || true
systemctl restart php8.4-fpm || true
fi
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Setting up NGINX - Might take a minute!"
printf "%b\n" "--------------------------------------------------------------------------"
apt-get install -y nginx
# Enable and start nginx
if command -v systemctl >/dev/null 2>&1; then
systemctl enable nginx || true
systemctl restart nginx || true
fi
# 3. CLONE OR UPDATE APPLICATION REPOSITORY
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Cloning application repository and setup"
printf "%b\n" "--------------------------------------------------------------------------"
mkdir -p "$INSTALL_DIR"
git clone -b baremetal-installer https://github.com/jokob-sk/NetAlertX.git "$INSTALL_DIR/" #change after testing
if [ ! -f "$INSTALL_DIR/front/buildtimestamp.txt" ]; then
date +%s > "$INSTALL_DIR/front/buildtimestamp.txt"
fi
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[FINISHED] ${RESET}NetAlertX Installation complete"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[CONFIGURATION] ${RESET}Configuring the web server"
printf "%b\n" "--------------------------------------------------------------------------"
# Stop any existing NetAlertX python server process (narrow pattern)
pkill -f "^python(3)?\s+.*${INSTALL_DIR}/server/?$" 2>/dev/null || true
# 4. SET UP PYTHON VIRTUAL ENVIRONMENT & DEPENDENCIES
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Setting up Python environment"
printf "%b\n" "--------------------------------------------------------------------------"
python3 -m venv /opt/myenv
source /opt/myenv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r "${INSTALLER_DIR}/requirements.txt"
# Backup default NGINX site just in case
if [ -L /etc/nginx/sites-enabled/default ] ; then
rm /etc/nginx/sites-enabled/default
elif [ -f /etc/nginx/sites-enabled/default ]; then
mv /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default.bkp_netalertx
fi
# Clear existing directories and files
if [ -d "$WEB_UI_DIR" ]; then
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[CHECKING] ${RESET}Removing existing NetAlertX web-UI"
printf "%b\n" "--------------------------------------------------------------------------"
rm -R "$WEB_UI_DIR"
fi
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[CHECKING] ${RESET}Removing existing NetAlertX NGINX config"
printf "%b\n" "--------------------------------------------------------------------------"
rm "$NGINX_CONFIG" 2>/dev/null || true
# Create web directory if it doesn't exist
mkdir -p /var/www/html
# create symbolic link to the installer directory
ln -sfn "${INSTALL_DIR}/front" "$WEB_UI_DIR"
# Copy NGINX configuration to NetAlertX config directory
cp "${INSTALLER_DIR}/${NGINX_CONF_FILE}" "${INSTALL_DIR}/config/${NGINX_CONF_FILE}"
# Use selected port (may be default 20211)
if [ -n "${PORT-}" ]; then
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "Setting webserver to port ($PORT)"
printf "%b\n" "--------------------------------------------------------------------------"
# Update the template to reflect the right port
sed -i "s/listen 20211;/listen ${PORT};/g" "${INSTALL_DIR}/config/${NGINX_CONF_FILE}"
# Warn if port is already in use
if ss -ltn | awk '{print $4}' | grep -q ":${PORT}$"; then
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${RED}[WARNING] ${RESET}Port ${PORT} appears in use. NGINX may fail to bind."
printf "%b\n" "--------------------------------------------------------------------------"
fi
fi
# Create symbolic link to NGINX configuration coming with NetAlertX
ln -sfn "${INSTALL_DIR}/config/${NGINX_CONF_FILE}" "${NGINX_CONFIG}"
# Run the hardware vendors update at least once
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[VENDORS UPDATE] ${RESET}Run the hardware vendors update"
printf "%b\n" "--------------------------------------------------------------------------"
# Check if ieee-oui.txt or ieee-iab.txt exist
if [ -f "$OUI_FILE" ]; then
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "The file ieee-oui.txt exists. Skipping update_vendors.sh..."
printf "%b\n" "--------------------------------------------------------------------------"
else
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "The file ieee-oui.txt does not exist. Running update_vendors..."
printf "%b\n" "--------------------------------------------------------------------------"
# Run the update_vendors.sh script
if [ -f "${INSTALL_DIR}/back/update_vendors.sh" ]; then
"${INSTALL_DIR}/back/update_vendors.sh"
else
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" " update_vendors.sh script not found in $INSTALL_DIR."
printf "%b\n" "--------------------------------------------------------------------------"
fi
fi
# Create empty log files and plugin folders
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Creating mounts and file structure"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "Cleaning up old mounts if any"
umount "${INSTALL_DIR}/log" 2>/dev/null || true
umount "${INSTALL_DIR}/api" 2>/dev/null || true
printf "%b\n" "Creating log api folders if they don't exist"
mkdir -p "${INSTALL_DIR}/log" "${INSTALL_DIR}/api"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Mounting log and api folders as tmpfs"
printf "%b\n" "--------------------------------------------------------------------------"
mountpoint -q "${INSTALL_DIR}/log" || mount -t tmpfs -o noexec,nosuid,nodev tmpfs "${INSTALL_DIR}/log"
mountpoint -q "${INSTALL_DIR}/api" || mount -t tmpfs -o noexec,nosuid,nodev tmpfs "${INSTALL_DIR}/api"
chown -R www-data:www-data "${INSTALL_DIR}/log" "${INSTALL_DIR}/api"
# Ensure plugins directory exists within the tmpfs mount
mkdir -p "${INSTALL_DIR}"/log/plugins
chown -R www-data:www-data "${INSTALL_DIR}"/log/plugins
# Create the execution_queue.log file if it doesn't exist
touch ${INSTALL_DIR}/log/{app.log,execution_queue.log,app_front.log,app.php_errors.log,stderr.log,stdout.log,db_is_locked.log}
touch ${INSTALL_DIR}/api/user_notifications.json
chown -R www-data:www-data "${INSTALL_DIR}"/log "${INSTALL_DIR}"/api
chmod -R ug+rwX "${INSTALL_DIR}"/log "${INSTALL_DIR}"/api
# Set ownership of the tmpfs mountpoints first.
chown www-data:www-data "${INSTALL_DIR}/log" "${INSTALL_DIR}/api"
# Ensure plugins directory exists within the tmpfs mount
mkdir -p "${INSTALL_DIR}/log/plugins"
# Create log and api files directly as the www-data user to ensure correct ownership from the start.
sudo -u www-data touch ${INSTALL_DIR}/log/{app.log,execution_queue.log,app_front.log,app.php_errors.log,stderr.log,stdout.log,db_is_locked.log}
sudo -u www-data touch ${INSTALL_DIR}/api/user_notifications.json
# Set final permissions for all created files and directories.
chown -R www-data:www-data "${INSTALL_DIR}/log" "${INSTALL_DIR}/api"
chmod -R ug+rwX "${INSTALL_DIR}/log" "${INSTALL_DIR}/api"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Setting up DB and CONF files"
printf "%b\n" "--------------------------------------------------------------------------"
# Copy starter $DB_FILE and $CONF_FILE
mkdir -p "${INSTALL_DIR}/config" "${INSTALL_DIR}/db"
cp -u "${INSTALL_DIR}/back/${CONF_FILE}" "${INSTALL_DIR}/config/${CONF_FILE}"
cp -u "${INSTALL_DIR}/back/${DB_FILE}" "${FILEDB}"
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[CONFIGURING] ${RESET}Setting File Permissions"
printf "%b\n" "--------------------------------------------------------------------------"
# Restrict wide permissions; allow owner/group access
chgrp -R www-data "$INSTALL_DIR"
chmod -R ug+rwX,o-rwx "$INSTALL_DIR"
chmod -R ug+rwX,o-rwx "$WEB_UI_DIR"
# chmod -R ug+rwX "$INSTALL_DIR/log" "$INSTALL_DIR/config"
chown -R www-data:www-data "$FILEDB" 2>/dev/null || true
# start PHP
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[STARTING] ${RESET}Starting PHP and NGINX"
printf "%b\n" "--------------------------------------------------------------------------"
/etc/init.d/php8.4-fpm start
nginx -t || {
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${RED}[ERROR] ${RESET}NGINX config test failed!"
printf "%b\n" "--------------------------------------------------------------------------"; exit 1; }
/etc/init.d/nginx start
# Make a start script
cat > "$INSTALL_DIR/start.netalertx.sh" << EOF
#!/usr/bin/env bash
# Activate the virtual python environment
source /opt/myenv/bin/activate
echo -e "--------------------------------------------------------------------------"
echo -e "Starting NetAlertX - navigate to http://${SERVER_IP}:${PORT}"
echo -e "--------------------------------------------------------------------------"
# Start the NetAlertX python script
python server/
EOF
chmod +x "$INSTALL_DIR/start.netalertx.sh"
# Install and manage systemd service if available, otherwise fallback to direct start
if command -v systemctl >/dev/null 2>&1; then
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Setting up systemd service"
printf "%b\n" "--------------------------------------------------------------------------"
cat > /etc/systemd/system/netalertx.service << 'EOF'
[Unit]
Description=NetAlertX Service
After=network-online.target nginx.service
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/app/start.netalertx.sh
WorkingDirectory=/app
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable/start service
systemctl daemon-reload
systemctl enable netalertx.service
systemctl start netalertx.service
systemctl restart nginx
# Verify service is running
if systemctl is-active --quiet netalertx.service; then
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[SUCCESS] ${RESET}NetAlertX service started successfully"
printf "%b\n" "--------------------------------------------------------------------------"
else
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${RED}[WARNING] ${RESET}NetAlertX service may not have started properly"
printf "%b\n" "--------------------------------------------------------------------------"
systemctl status netalertx.service --no-pager -l
fi
else
printf "%b\n" "--------------------------------------------------------------------------"
printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Starting NetAlertX (no systemd)"
printf "%b\n" "--------------------------------------------------------------------------"
"$INSTALL_DIR/start.netalertx.sh" &
fi
echo -e "--------------------------------------------------------------------------"
echo -e "${GREEN}[Service] 🚀 Starting app - navigate to http://${SERVER_IP}:${PORT}"
echo -e "--------------------------------------------------------------------------"

View File

@@ -0,0 +1,25 @@
openwrt-luci-rpc
asusrouter
aiohttp
graphene
flask
flask-cors
unifi-sm-api
tplink-omada-client
wakeonlan
pycryptodome
requests
paho-mqtt
scapy
cron-converter
pytz
json2table
dhcp-leases
pyunifi
speedtest-cli
chardet
python-nmap
dnspython
librouteros
yattag
git+https://github.com/foreign-sub/aiofreepybox.git

View File

@@ -153,47 +153,259 @@ class SafeConditionBuilder:
def _parse_condition(self, condition: str) -> Tuple[str, Dict[str, Any]]:
"""
Parse a condition string into safe SQL with parameters.
This method handles basic patterns like:
- AND devName = 'value'
- AND devComments LIKE '%value%'
- AND eve_EventType IN ('type1', 'type2')
This method handles both single and compound conditions:
- Single: AND devName = 'value'
- Compound: AND devName = 'value' AND devVendor = 'Apple'
- Multiple clauses with AND/OR operators
Args:
condition: Condition string to parse
Returns:
Tuple of (safe_sql_snippet, parameters_dict)
"""
condition = condition.strip()
# Handle empty conditions
if not condition:
return "", {}
# Check if this is a compound condition (multiple clauses)
if self._is_compound_condition(condition):
return self._parse_compound_condition(condition)
# Single condition: extract leading logical operator if present
logical_op = None
clause_text = condition
# Check for leading 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'
clause_text = condition[2:].strip()
# Parse the single condition
return self._parse_single_condition(clause_text, logical_op)
def _is_compound_condition(self, condition: str) -> bool:
"""
Determine if a condition contains multiple clauses (compound condition).
A compound condition has multiple logical operators (AND/OR) connecting
separate comparison clauses.
Args:
condition: Condition string to check
Returns:
True if compound (multiple clauses), False if single clause
"""
# Track if we're inside quotes to avoid counting operators in quoted strings
in_quotes = False
logical_op_count = 0
i = 0
while i < len(condition):
char = condition[i]
# Toggle quote state
if char == "'":
in_quotes = not in_quotes
i += 1
continue
# Only count logical operators outside of quotes
if not in_quotes:
# Look for AND or OR as whole words
remaining = condition[i:].upper()
# Check for AND (must be word boundary)
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'):
logical_op_count += 1
i += 2
continue
i += 1
# A compound condition has more than one logical operator
# (first AND/OR starts the condition, subsequent ones connect clauses)
return logical_op_count > 1
def _parse_compound_condition(self, condition: str) -> Tuple[str, Dict[str, Any]]:
"""
Parse a compound condition with multiple clauses.
Splits the condition into individual clauses, parses each one,
and reconstructs the full condition with all parameters.
Args:
condition: Compound condition string
Returns:
Tuple of (safe_sql_snippet, parameters_dict)
"""
# Split the condition into individual clauses while preserving logical operators
clauses = self._split_by_logical_operators(condition)
# Parse each clause individually
parsed_parts = []
all_params = {}
for clause_text, logical_op in clauses:
# Parse this single clause
sql_part, params = self._parse_single_condition(clause_text, logical_op)
if sql_part:
parsed_parts.append(sql_part)
all_params.update(params)
if not parsed_parts:
raise ValueError("No valid clauses found in compound condition")
# Join all parsed parts
final_sql = " ".join(parsed_parts)
return final_sql, all_params
def _split_by_logical_operators(self, condition: str) -> List[Tuple[str, Optional[str]]]:
"""
Split a compound condition into individual clauses.
Returns a list of tuples: (clause_text, logical_operator)
The logical operator is the AND/OR that precedes the clause.
Args:
condition: Compound condition string
Returns:
List of (clause_text, logical_op) tuples
"""
clauses = []
current_clause = []
current_logical_op = None
in_quotes = False
i = 0
while i < len(condition):
char = condition[i]
# Toggle quote state
if char == "'":
in_quotes = not in_quotes
current_clause.append(char)
i += 1
continue
# Only look for logical operators outside of quotes
if not in_quotes:
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')
# Check for AND (must be at word boundary)
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()
if clause_text:
clauses.append((clause_text, current_logical_op))
current_clause = []
# Set the logical operator for the next clause
current_logical_op = 'AND'
i += 3 # Skip 'AND'
# Skip whitespace after AND
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')):
# Save current clause if we have one
if current_clause:
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'
i += 2 # Skip 'OR'
# Skip whitespace after OR
while i < len(condition) and condition[i] in ' \t':
i += 1
continue
# Add character to current clause
current_clause.append(char)
i += 1
# Don't forget the last clause
if current_clause:
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]]:
"""
Parse a single condition clause into safe SQL with parameters.
This method handles basic patterns like:
- devName = 'value' (with optional AND/OR prefix)
- devComments LIKE '%value%'
- eve_EventType IN ('type1', 'type2')
Args:
condition: Single condition string to parse
logical_op: Optional logical operator (AND/OR) to prepend
Returns:
Tuple of (safe_sql_snippet, parameters_dict)
"""
condition = condition.strip()
# Handle empty conditions
if not condition:
return "", {}
# Simple pattern matching for common conditions
# Pattern 1: AND/OR column operator value (supporting Unicode in quoted strings)
pattern1 = r'^\s*(AND|OR)?\s+(\w+)\s+(=|!=|<>|<|>|<=|>=|LIKE|NOT\s+LIKE)\s+\'([^\']*)\'\s*$'
# Pattern 1: [AND/OR] column operator value (supporting Unicode in quoted strings)
pattern1 = r'^\s*(\w+)\s+(=|!=|<>|<|>|<=|>=|LIKE|NOT\s+LIKE)\s+\'([^\']*)\'\s*$'
match1 = re.match(pattern1, condition, re.IGNORECASE | re.UNICODE)
if match1:
logical_op, column, operator, value = match1.groups()
column, operator, value = match1.groups()
return self._build_simple_condition(logical_op, column, operator, value)
# Pattern 2: AND/OR column IN ('val1', 'val2', ...)
pattern2 = r'^\s*(AND|OR)?\s+(\w+)\s+(IN|NOT\s+IN)\s+\(([^)]+)\)\s*$'
# Pattern 2: [AND/OR] column IN ('val1', 'val2', ...)
pattern2 = r'^\s*(\w+)\s+(IN|NOT\s+IN)\s+\(([^)]+)\)\s*$'
match2 = re.match(pattern2, condition, re.IGNORECASE)
if match2:
logical_op, column, operator, values_str = match2.groups()
column, operator, values_str = match2.groups()
return self._build_in_condition(logical_op, column, operator, values_str)
# Pattern 3: AND/OR column IS NULL/IS NOT NULL
pattern3 = r'^\s*(AND|OR)?\s+(\w+)\s+(IS\s+NULL|IS\s+NOT\s+NULL)\s*$'
# Pattern 3: [AND/OR] column IS NULL/IS NOT NULL
pattern3 = r'^\s*(\w+)\s+(IS\s+NULL|IS\s+NOT\s+NULL)\s*$'
match3 = re.match(pattern3, condition, re.IGNORECASE)
if match3:
logical_op, column, operator = match3.groups()
column, operator = match3.groups()
return self._build_null_condition(logical_op, column, operator)
# If no patterns match, reject the condition for security

View File

@@ -0,0 +1,326 @@
"""
Unit tests for SafeConditionBuilder compound condition parsing.
Tests the fix for Issue #1210 - compound conditions with multiple AND/OR clauses.
"""
import sys
import unittest
from unittest.mock import MagicMock
# Mock the logger module before importing SafeConditionBuilder
sys.modules['logger'] = MagicMock()
# Add parent directory to path for imports
sys.path.insert(0, '/tmp/netalertx_hotfix/server/db')
from sql_safe_builder import SafeConditionBuilder
class TestCompoundConditions(unittest.TestCase):
"""Test compound condition parsing functionality."""
def setUp(self):
"""Create a fresh builder instance for each test."""
self.builder = SafeConditionBuilder()
def test_user_failing_filter_six_and_clauses(self):
"""Test the exact user-reported failing filter from Issue #1210."""
condition = (
"AND devLastIP NOT LIKE '192.168.50.%' "
"AND devLastIP NOT LIKE '192.168.60.%' "
"AND devLastIP NOT LIKE '192.168.70.2' "
"AND devLastIP NOT LIKE '192.168.70.5' "
"AND devLastIP NOT LIKE '192.168.70.3' "
"AND devLastIP NOT LIKE '192.168.70.4'"
)
sql, params = self.builder.build_safe_condition(condition)
# Should successfully parse
self.assertIsNotNone(sql)
self.assertIsNotNone(params)
# Should have 6 parameters (one per clause)
self.assertEqual(len(params), 6)
# Should contain all 6 AND operators
self.assertEqual(sql.count('AND'), 6)
# Should contain all 6 NOT LIKE operators
self.assertEqual(sql.count('NOT LIKE'), 6)
# Should have 6 parameter placeholders
self.assertEqual(sql.count(':param_'), 6)
# Verify all IP patterns are in parameters
param_values = list(params.values())
self.assertIn('192.168.50.%', param_values)
self.assertIn('192.168.60.%', param_values)
self.assertIn('192.168.70.2', param_values)
self.assertIn('192.168.70.5', param_values)
self.assertIn('192.168.70.3', param_values)
self.assertIn('192.168.70.4', param_values)
def test_multiple_and_clauses_simple(self):
"""Test multiple AND clauses with simple equality operators."""
condition = "AND devName = 'Device1' AND devVendor = 'Apple' AND devFavorite = '1'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 3 parameters
self.assertEqual(len(params), 3)
# Should have 3 AND operators
self.assertEqual(sql.count('AND'), 3)
# Verify all values are parameterized
param_values = list(params.values())
self.assertIn('Device1', param_values)
self.assertIn('Apple', param_values)
self.assertIn('1', param_values)
def test_multiple_or_clauses(self):
"""Test multiple OR clauses."""
condition = "OR devName = 'Device1' OR devName = 'Device2' OR devName = 'Device3'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 3 parameters
self.assertEqual(len(params), 3)
# Should have 3 OR operators
self.assertEqual(sql.count('OR'), 3)
# Verify all device names are parameterized
param_values = list(params.values())
self.assertIn('Device1', param_values)
self.assertIn('Device2', param_values)
self.assertIn('Device3', param_values)
def test_mixed_and_or_clauses(self):
"""Test mixed AND/OR logical operators."""
condition = "AND devName = 'Device1' OR devName = 'Device2' AND devFavorite = '1'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 3 parameters
self.assertEqual(len(params), 3)
# Should preserve the logical operator order
self.assertIn('AND', sql)
self.assertIn('OR', sql)
# Verify all values are parameterized
param_values = list(params.values())
self.assertIn('Device1', param_values)
self.assertIn('Device2', param_values)
self.assertIn('1', param_values)
def test_single_condition_backward_compatibility(self):
"""Test that single conditions still work (backward compatibility)."""
condition = "AND devName = 'TestDevice'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 1 parameter
self.assertEqual(len(params), 1)
# Should match expected format
self.assertIn('AND devName = :param_', sql)
# Parameter should contain the value
self.assertIn('TestDevice', params.values())
def test_single_condition_like_operator(self):
"""Test single LIKE condition for backward compatibility."""
condition = "AND devComments LIKE '%important%'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 1 parameter
self.assertEqual(len(params), 1)
# Should contain LIKE operator
self.assertIn('LIKE', sql)
# Parameter should contain the pattern
self.assertIn('%important%', params.values())
def test_compound_with_like_patterns(self):
"""Test compound conditions with LIKE patterns."""
condition = "AND devLastIP LIKE '192.168.%' AND devVendor LIKE '%Apple%'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 2 parameters
self.assertEqual(len(params), 2)
# Should have 2 LIKE operators
self.assertEqual(sql.count('LIKE'), 2)
# Verify patterns are parameterized
param_values = list(params.values())
self.assertIn('192.168.%', param_values)
self.assertIn('%Apple%', param_values)
def test_compound_with_inequality_operators(self):
"""Test compound conditions with various inequality operators."""
condition = "AND eve_DateTime > '2024-01-01' AND eve_DateTime < '2024-12-31'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 2 parameters
self.assertEqual(len(params), 2)
# Should have both operators
self.assertIn('>', sql)
self.assertIn('<', sql)
# Verify dates are parameterized
param_values = list(params.values())
self.assertIn('2024-01-01', param_values)
self.assertIn('2024-12-31', param_values)
def test_empty_condition(self):
"""Test empty condition string."""
condition = ""
sql, params = self.builder.build_safe_condition(condition)
# Should return empty results
self.assertEqual(sql, "")
self.assertEqual(params, {})
def test_whitespace_only_condition(self):
"""Test condition with only whitespace."""
condition = " \t\n "
sql, params = self.builder.build_safe_condition(condition)
# Should return empty results
self.assertEqual(sql, "")
self.assertEqual(params, {})
def test_invalid_column_name_rejected(self):
"""Test that invalid column names are rejected."""
condition = "AND malicious_column = 'value'"
with self.assertRaises(ValueError):
self.builder.build_safe_condition(condition)
def test_invalid_operator_rejected(self):
"""Test that invalid operators are rejected."""
condition = "AND devName EXECUTE 'DROP TABLE'"
with self.assertRaises(ValueError):
self.builder.build_safe_condition(condition)
def test_sql_injection_attempt_blocked(self):
"""Test that SQL injection attempts are blocked."""
condition = "AND devName = 'value'; DROP TABLE devices; --"
# Should either reject or sanitize the dangerous input
# The semicolon and comment should not appear in the final SQL
try:
sql, params = self.builder.build_safe_condition(condition)
# If it doesn't raise an error, it should sanitize the input
self.assertNotIn('DROP', sql.upper())
self.assertNotIn(';', sql)
except ValueError:
# Rejection is also acceptable
pass
def test_quoted_string_with_spaces(self):
"""Test that quoted strings with spaces are handled correctly."""
condition = "AND devName = 'My Device Name' AND devComments = 'Has spaces here'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 2 parameters
self.assertEqual(len(params), 2)
# Verify values with spaces are preserved
param_values = list(params.values())
self.assertIn('My Device Name', param_values)
self.assertIn('Has spaces here', param_values)
def test_compound_condition_with_not_equal(self):
"""Test compound conditions with != operator."""
condition = "AND devName != 'Device1' AND devVendor != 'Unknown'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 2 parameters
self.assertEqual(len(params), 2)
# Should have != operators (or converted to <>)
self.assertTrue('!=' in sql or '<>' in sql)
# Verify values are parameterized
param_values = list(params.values())
self.assertIn('Device1', param_values)
self.assertIn('Unknown', param_values)
def test_very_long_compound_condition(self):
"""Test handling of very long compound conditions (10+ clauses)."""
clauses = []
for i in range(10):
clauses.append(f"AND devName != 'Device{i}'")
condition = " ".join(clauses)
sql, params = self.builder.build_safe_condition(condition)
# Should have 10 parameters
self.assertEqual(len(params), 10)
# Should have 10 AND operators
self.assertEqual(sql.count('AND'), 10)
# Verify all device names are parameterized
param_values = list(params.values())
for i in range(10):
self.assertIn(f'Device{i}', param_values)
class TestParameterGeneration(unittest.TestCase):
"""Test parameter generation and naming."""
def setUp(self):
"""Create a fresh builder instance for each test."""
self.builder = SafeConditionBuilder()
def test_parameters_have_unique_names(self):
"""Test that all parameters get unique names."""
condition = "AND devName = 'A' AND devName = 'B' AND devName = 'C'"
sql, params = self.builder.build_safe_condition(condition)
# All parameter names should be unique
param_names = list(params.keys())
self.assertEqual(len(param_names), len(set(param_names)))
def test_parameter_values_match_condition(self):
"""Test that parameter values correctly match the condition values."""
condition = "AND devLastIP NOT LIKE '192.168.1.%' AND devLastIP NOT LIKE '10.0.0.%'"
sql, params = self.builder.build_safe_condition(condition)
# Should have exactly the values from the condition
param_values = sorted(params.values())
expected_values = sorted(['192.168.1.%', '10.0.0.%'])
self.assertEqual(param_values, expected_values)
def test_parameters_referenced_in_sql(self):
"""Test that all parameters are actually referenced in the SQL."""
condition = "AND devName = 'Device1' AND devVendor = 'Apple'"
sql, params = self.builder.build_safe_condition(condition)
# Every parameter should appear in the SQL
for param_name in params.keys():
self.assertIn(f':{param_name}', sql)
if __name__ == '__main__':
unittest.main()