Merge branch 'main' of https://github.com/jokob-sk/NetAlertX
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled

This commit is contained in:
jokob-sk
2025-11-03 08:14:26 +11:00
77 changed files with 5196 additions and 1003 deletions

View File

@@ -72,11 +72,13 @@ ENV LOG_STDOUT=${NETALERTX_LOG}/stdout.log
ENV LOG_CROND=${NETALERTX_LOG}/crond.log
# System Services configuration files
ENV ENTRYPOINT_CHECKS=/entrypoint.d
ENV SYSTEM_SERVICES=/services
ENV SYSTEM_SERVICES_SCRIPTS=${SYSTEM_SERVICES}/scripts
ENV SYSTEM_SERVICES_CONFIG=${SYSTEM_SERVICES}/config
ENV SYSTEM_NGINX_CONFIG=${SYSTEM_SERVICES_CONFIG}/nginx
ENV SYSTEM_NGINX_CONFIG_FILE=${SYSTEM_NGINX_CONFIG}/nginx.conf
ENV SYSTEM_SERVICES_ACTIVE_CONFIG=${SYSTEM_NGINX_CONFIG}/conf.active
ENV SYSTEM_SERVICES_PHP_FOLDER=${SYSTEM_SERVICES_CONFIG}/php
ENV SYSTEM_SERVICES_PHP_FPM_D=${SYSTEM_SERVICES_PHP_FOLDER}/php-fpm.d
ENV SYSTEM_SERVICES_CROND=${SYSTEM_SERVICES_CONFIG}/crond
@@ -85,7 +87,7 @@ ENV SYSTEM_SERVICES_RUN_TMP=${SYSTEM_SERVICES_RUN}/tmp
ENV SYSTEM_SERVICES_RUN_LOG=${SYSTEM_SERVICES_RUN}/logs
ENV PHP_FPM_CONFIG_FILE=${SYSTEM_SERVICES_PHP_FOLDER}/php-fpm.conf
ENV READ_ONLY_FOLDERS="${NETALERTX_BACK} ${NETALERTX_FRONT} ${NETALERTX_SERVER} ${SYSTEM_SERVICES} \
${SYSTEM_SERVICES_CONFIG}"
${SYSTEM_SERVICES_CONFIG} ${ENTRYPOINT_CHECKS}"
ENV READ_WRITE_FOLDERS="${NETALERTX_CONFIG} ${NETALERTX_DB} ${NETALERTX_API} ${NETALERTX_LOG} \
${NETALERTX_PLUGINS_LOG} ${SYSTEM_SERVICES_RUN} ${SYSTEM_SERVICES_RUN_TMP} \
${SYSTEM_SERVICES_RUN_LOG}"
@@ -184,7 +186,7 @@ RUN chown -R ${READ_ONLY_USER}:${READ_ONLY_GROUP} ${READ_ONLY_FOLDERS} && \
chmod -R 600 ${READ_WRITE_FOLDERS} && \
find ${READ_WRITE_FOLDERS} -type d -exec chmod 700 {} + && \
chown ${READ_ONLY_USER}:${READ_ONLY_GROUP} /entrypoint.sh /opt /opt/venv && \
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh /app /opt /opt/venv && \
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh ${SYSTEM_SERVICES_SCRIPTS}/* ${ENTRYPOINT_CHECKS}/* /app /opt /opt/venv && \
for dir in ${READ_WRITE_FOLDERS}; do \
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 "$dir"; \
done && \

View File

@@ -69,11 +69,13 @@ ENV LOG_STDOUT=${NETALERTX_LOG}/stdout.log
ENV LOG_CROND=${NETALERTX_LOG}/crond.log
# System Services configuration files
ENV ENTRYPOINT_CHECKS=/entrypoint.d
ENV SYSTEM_SERVICES=/services
ENV SYSTEM_SERVICES_SCRIPTS=${SYSTEM_SERVICES}/scripts
ENV SYSTEM_SERVICES_CONFIG=${SYSTEM_SERVICES}/config
ENV SYSTEM_NGINX_CONFIG=${SYSTEM_SERVICES_CONFIG}/nginx
ENV SYSTEM_NGINX_CONFIG_FILE=${SYSTEM_NGINX_CONFIG}/nginx.conf
ENV SYSTEM_SERVICES_ACTIVE_CONFIG=${SYSTEM_NGINX_CONFIG}/conf.active
ENV SYSTEM_SERVICES_PHP_FOLDER=${SYSTEM_SERVICES_CONFIG}/php
ENV SYSTEM_SERVICES_PHP_FPM_D=${SYSTEM_SERVICES_PHP_FOLDER}/php-fpm.d
ENV SYSTEM_SERVICES_CROND=${SYSTEM_SERVICES_CONFIG}/crond
@@ -82,10 +84,10 @@ ENV SYSTEM_SERVICES_RUN_TMP=${SYSTEM_SERVICES_RUN}/tmp
ENV SYSTEM_SERVICES_RUN_LOG=${SYSTEM_SERVICES_RUN}/logs
ENV PHP_FPM_CONFIG_FILE=${SYSTEM_SERVICES_PHP_FOLDER}/php-fpm.conf
ENV READ_ONLY_FOLDERS="${NETALERTX_BACK} ${NETALERTX_FRONT} ${NETALERTX_SERVER} ${SYSTEM_SERVICES} \
${SYSTEM_SERVICES_CONFIG}"
${SYSTEM_SERVICES_CONFIG} ${ENTRYPOINT_CHECKS}"
ENV READ_WRITE_FOLDERS="${NETALERTX_CONFIG} ${NETALERTX_DB} ${NETALERTX_API} ${NETALERTX_LOG} \
${NETALERTX_PLUGINS_LOG} ${SYSTEM_SERVICES_RUN} ${SYSTEM_SERVICES_RUN_TMP} \
${SYSTEM_SERVICES_RUN_LOG}"
${SYSTEM_SERVICES_RUN_LOG} ${SYSTEM_NGINX_CONFIG}"
#Python environment
ENV PYTHONUNBUFFERED=1
@@ -181,7 +183,7 @@ RUN chown -R ${READ_ONLY_USER}:${READ_ONLY_GROUP} ${READ_ONLY_FOLDERS} && \
chmod -R 600 ${READ_WRITE_FOLDERS} && \
find ${READ_WRITE_FOLDERS} -type d -exec chmod 700 {} + && \
chown ${READ_ONLY_USER}:${READ_ONLY_GROUP} /entrypoint.sh /opt /opt/venv && \
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh /app /opt /opt/venv && \
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh ${SYSTEM_SERVICES_SCRIPTS}/* ${ENTRYPOINT_CHECKS}/* /app /opt /opt/venv && \
for dir in ${READ_WRITE_FOLDERS}; do \
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 "$dir"; \
done && \

View File

@@ -0,0 +1,32 @@
# Excessive Capabilities
## Issue Description
Excessive Linux capabilities are detected beyond the necessary NET_ADMIN, NET_BIND_SERVICE, and NET_RAW. This may indicate overly permissive container configuration.
## Security Ramifications
While the detected capabilities might not directly harm operation, running with more privileges than necessary increases the attack surface. If the container is compromised, additional capabilities could allow broader system access or privilege escalation.
## Why You're Seeing This Issue
This occurs when your Docker configuration grants more capabilities than required for network monitoring. The application only needs specific network-related capabilities for proper function.
## How to Correct the Issue
Limit capabilities to only those required:
- In docker-compose.yml, specify only needed caps:
```yaml
cap_add:
- NET_RAW
- NET_ADMIN
- NET_BIND_SERVICE
```
- Remove any unnecessary `--cap-add` flags from docker run commands
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,27 @@
# File Permission Issues
## Issue Description
NetAlertX cannot read from or write to critical configuration and database files. This prevents the application from saving data, logs, or configuration changes.
## Security Ramifications
Incorrect file permissions can expose sensitive configuration data or database contents to unauthorized access. Network monitoring tools handle sensitive information about devices on your network, and improper permissions could lead to information disclosure.
## Why You're Seeing This Issue
This occurs when the mounted volumes for configuration and database files don't have proper ownership or permissions set for the netalertx user (UID 20211). The container expects these files to be accessible by the service account, not root or other users.
## How to Correct the Issue
Fix permissions on the host system for the mounted directories:
- Ensure the config and database directories are owned by the netalertx user: `chown -R 20211:20211 /path/to/config /path/to/db`
- Set appropriate permissions: `chmod -R 755 /path/to/config /path/to/db` for directories, `chmod 644` for files
- Alternatively, restart the container with root privileges temporarily to allow automatic permission fixing, then switch back to the default user
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,28 @@
# Incorrect Container User
## Issue Description
NetAlertX is running as UID:GID other than the expected 20211:20211. This bypasses hardened permissions, file ownership, and runtime isolation safeguards.
## Security Ramifications
The application is designed with security hardening that depends on running under a dedicated, non-privileged service account. Using a different user account can silently fail future upgrades and removes crucial isolation between the container and host system.
## Why You're Seeing This Issue
This occurs when you override the container's default user with custom `user:` directives in docker-compose.yml or `--user` flags in docker run commands. The container expects to run as the netalertx user for proper security isolation.
## How to Correct the Issue
Restore the container to the default user:
- Remove any `user:` overrides from docker-compose.yml
- Avoid `--user` flags in docker run commands
- Allow the container to run with its default UID:GID 20211:20211
- Recreate the container so volume ownership is reset automatically
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,32 @@
# Missing Network Capabilities
## Issue Description
Raw network capabilities (NET_RAW, NET_ADMIN, NET_BIND_SERVICE) are missing. Tools that rely on these capabilities (e.g., nmap -sS, arp-scan, nbtscan) will not function.
## Security Ramifications
Network scanning and monitoring requires low-level network access that these capabilities provide. Without them, the application cannot perform essential functions like ARP scanning, port scanning, or passive network discovery, severely limiting its effectiveness.
## Why You're Seeing This Issue
This occurs when the container doesn't have the necessary Linux capabilities granted. Docker containers run with limited capabilities by default, and network monitoring tools need elevated network privileges.
## How to Correct the Issue
Add the required capabilities to your container:
- In docker-compose.yml:
```yaml
cap_add:
- NET_RAW
- NET_ADMIN
- NET_BIND_SERVICE
```
- For docker run: `--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=NET_BIND_SERVICE`
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,36 @@
# Mount Configuration Issues
## Issue Description
NetAlertX has detected configuration issues with your Docker volume mounts. These may include write permission problems, data loss risks, or performance concerns marked with ❌ in the table.
## Security Ramifications
Improper mount configurations can lead to data loss, performance degradation, or security vulnerabilities. For persistent data (database and configuration), using non-persistent storage like tmpfs can result in complete data loss on container restart. For temporary data, using persistent storage may unnecessarily expose sensitive logs or cache data.
## Why You're Seeing This Issue
This occurs when your Docker Compose or run configuration doesn't properly map host directories to container paths, or when the mounted volumes have incorrect permissions. The application requires specific paths to be writable for operation, and some paths should use persistent storage while others should be temporary.
## How to Correct the Issue
Review and correct your volume mounts in docker-compose.yml:
- Ensure `${NETALERTX_DB}` and `${NETALERTX_CONFIG}` use persistent host directories
- Ensure `${NETALERTX_API}`, `${NETALERTX_LOG}` have appropriate permissions
- Avoid mounting sensitive paths to non-persistent filesystems like tmpfs for critical data
- Use bind mounts with proper ownership (netalertx user: 20211:20211)
Example volume configuration:
```yaml
volumes:
- ./data/db:/app/db
- ./data/config:/app/config
- ./data/log:/app/log
```
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,27 @@
# Network Mode Configuration
## Issue Description
NetAlertX is not running with `--network=host`. Bridge networking blocks passive discovery (ARP, NBNS, mDNS) and active scanning accuracy.
## Security Ramifications
Host networking is required for comprehensive network monitoring. Bridge mode isolates the container from raw network access needed for ARP scanning, passive discovery protocols, and accurate device detection. Without host networking, the application cannot fully monitor your network.
## Why You're Seeing This Issue
This occurs when your Docker configuration uses bridge networking instead of host networking. Network monitoring requires direct access to the host's network interfaces to perform passive discovery and active scanning.
## How to Correct the Issue
Enable host networking mode:
- In docker-compose.yml, add: `network_mode: host`
- For docker run, use: `--network=host`
- Ensure the container has required capabilities: `--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=NET_BIND_SERVICE`
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,36 @@
# Nginx Configuration Mount Issues
## Issue Description
You've configured a custom port for NetAlertX, but the required nginx configuration mount is missing or not writable. Without this mount, the container cannot apply your port changes and will fall back to the default port 20211.
## Security Ramifications
Running in read-only mode (as recommended) prevents the container from modifying its own nginx configuration. Without a writable mount, custom port configurations cannot be applied, potentially exposing the service on unintended ports or requiring fallback to defaults.
## Why You're Seeing This Issue
This occurs when you set a custom PORT environment variable (other than 20211) but haven't provided a writable mount for nginx configuration. The container needs to write custom nginx config files when running in read-only mode.
## How to Correct the Issue
If you want to use a custom port, create a bind mount for the nginx configuration:
- Create a directory on your host: `mkdir -p /path/to/nginx-config`
- Add to your docker-compose.yml:
```yaml
volumes:
- /path/to/nginx-config:/app/system/services/active/config
environment:
- PORT=your_custom_port
```
- Ensure it's owned by the netalertx user: `chown -R 20211:20211 /path/to/nginx-config`
- Set permissions: `chmod -R 700 /path/to/nginx-config`
If you don't need a custom port, simply omit the PORT environment variable and the container will use 20211 by default.
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,86 @@
# Port Conflicts
## Issue Description
The configured application port (default 20211) or GraphQL API port (default 20212) is already in use by another service. This commonly occurs when you already have another NetAlertX instance running.
## Security Ramifications
Port conflicts prevent the application from starting properly, leaving network monitoring services unavailable. Running multiple instances on the same ports can also create configuration confusion and potential security issues if services are inadvertently exposed.
## Why You're Seeing This Issue
This error typically occurs when:
- **You already have NetAlertX running** - Another Docker container or devcontainer instance is using the default ports 20211 and 20212
- **Port conflicts with other services** - Other applications on your system are using these ports
- **Configuration error** - Both PORT and GRAPHQL_PORT environment variables are set to the same value
## How to Correct the Issue
### Check for Existing NetAlertX Instances
First, check if you already have NetAlertX running:
```bash
# Check for running NetAlertX containers
docker ps | grep netalertx
# Check for devcontainer processes
ps aux | grep netalertx
# Check what services are using the ports
netstat -tlnp | grep :20211
netstat -tlnp | grep :20212
```
### Stop Conflicting Instances
If you find another NetAlertX instance:
```bash
# Stop specific container
docker stop <container_name>
# Stop all NetAlertX containers
docker stop $(docker ps -q --filter ancestor=jokob-sk/netalertx)
# Stop devcontainer services
# Use VS Code command palette: "Dev Containers: Rebuild Container"
```
### Configure Different Ports
If you need multiple instances, configure unique ports:
```yaml
environment:
- PORT=20211 # Main application port
- GRAPHQL_PORT=20212 # GraphQL API port
```
For a second instance, use different ports:
```yaml
environment:
- PORT=20213 # Different main port
- GRAPHQL_PORT=20214 # Different API port
```
### Alternative: Use Different Container Names
When running multiple instances, use unique container names:
```yaml
services:
netalertx-primary:
# ... existing config
netalertx-secondary:
# ... config with different ports
```
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,27 @@
# Read-Only Filesystem Mode
## Issue Description
The container is running as read-write instead of read-only mode. This reduces the security hardening of the appliance.
## Security Ramifications
Read-only root filesystem is a security best practice that prevents malicious modifications to the container's filesystem. Running read-write allows potential attackers to modify system files or persist malware within the container.
## Why You're Seeing This Issue
This occurs when the Docker configuration doesn't mount the root filesystem as read-only. The application is designed as a security appliance that should prevent filesystem modifications.
## How to Correct the Issue
Enable read-only mode:
- In docker-compose.yml, add: `read_only: true`
- For docker run, use: `--read-only`
- Ensure necessary directories are mounted as writable volumes (tmp, logs, etc.)
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -0,0 +1,29 @@
# Running as Root User
## Issue Description
NetAlertX has detected that the container is running with root privileges (UID 0). This configuration bypasses all built-in security hardening measures designed to protect your system.
## Security Ramifications
Running security-critical applications like network monitoring tools as root grants unrestricted access to your host system. A successful compromise here could jeopardize your entire infrastructure, including other containers, host services, and potentially your network.
## Why You're Seeing This Issue
This typically occurs when you've explicitly overridden the container's default user in your Docker configuration, such as using `user: root` or `--user 0:0` in docker-compose.yml or docker run commands. The application is designed to run under a dedicated, non-privileged service account for security.
## How to Correct the Issue
Switch to the dedicated 'netalertx' user by removing any custom user directives:
- Remove `user:` entries from your docker-compose.yml
- Avoid `--user` flags in docker run commands
- Ensure the container runs with the default UID 20211:20211
After making these changes, restart the container. The application will automatically adjust ownership of required directories.
## Additional Resources
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)

View File

@@ -1,21 +1,28 @@
## Overview
NMAP-scan is a command-line tool to discover and fingerprint IP hosts on the local network. The NMAP-scan (and other Network-scan plugin times using the `SCAN_SUBNETS` setting) time depends on the number of IP addresses to check so set this up carefully with the appropriate network mask and interface. Check the [subnets documentation](https://github.com/jokob-sk/NetAlertX/blob/main/docs/SUBNETS.md) for help with setting up VLANs, what VLANs are supported, or how to figure out the network mask and your interface.
**NMAP-scan** is a command-line tool used to discover and fingerprint IP hosts on your network.
The NMAP-scan (and other Network-scan plugins using the `SCAN_SUBNETS` setting) runtime depends on the number of IP addresses to check — so configure it carefully with the appropriate **network mask** and **interface**.
Refer to the [subnets documentation](https://github.com/jokob-sk/NetAlertX/blob/main/docs/SUBNETS.md) for help with setting up VLANs, understanding which VLANs are supported, and determining your network mask and interface.
> [!NOTE]
> The `NMAPDEV` plugin is great for detecting the availability of devices, however ARP scan might be better covering multiple VLANS and subnets as NMAP can't pickup the MAC address from other subnets (this is an NMAP limitation) which are necessary to identify a device. You can always combine different scan methods. You can find all available network scanning options (marked as `🔍 dev scanner`) in the [Plugins overview](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md) readme.
> The `NMAPDEV` plugin is excellent for detecting device availability, but **ARP-scan** is better for scanning across multiple VLANs and subnets.
> NMAP cannot retrieve MAC addresses from other subnets (an NMAP limitation), which are often required to identify devices.
> You can safely combine different scan methods.
> See all available network scanning options (marked with `🔍 dev scanner`) in the [Plugins overview](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md).
This plugin is **not optimized for name resolution** (use `NSLOOKUP` or `AVAHISCAN` instead), but if a name is available it will appear in the **Resolved Name** column.
This plugin is not the best for name resolution (Use e.g.: `NSLOOKUP`, `AVAHISCAN` instead), however if available a name will be displayed in the `Resolved Name` column.
---
### Usage
- Go to settings and set the `SCAN_SUBNETS` setting as per [subnets documentation](https://github.com/jokob-sk/NetAlertX/blob/main/docs/SUBNETS.md).
- Enable the plugin by changing the RUN parameter from disabled to your preferred run time (usually: `schedule`).
- Specify the schedule in the `NMAPDEV_RUN_SCHD` setting
- Adjust the timeout if needed in the `NMAPDEV_RUN_TIMEOUT` setting
- If scanning remote networks you may want to enable the `NMAPDEV_FAKE_MAC` setting. Please read the setting description carefully.
- Review remaining settings
- SAVE
- Wait for the next scan to finish
1. In **Settings**, configure the `SCAN_SUBNETS` value as described in the [subnets documentation](https://github.com/jokob-sk/NetAlertX/blob/main/docs/SUBNETS.md).
The plugin automatically **strips unsupported `--vlan` parameters** and replaces `--interface` with `-e`.
2. Enable the plugin by setting the `RUN` parameter from `disabled` to your preferred run mode (usually `schedule`).
3. Specify the schedule using the `NMAPDEV_RUN_SCHD` setting.
4. Adjust the scan timeout if necessary with the `NMAPDEV_RUN_TIMEOUT` setting.
5. If scanning **remote networks**, consider enabling the `NMAPDEV_FAKE_MAC` setting — review its description carefully before use.
6. Review all remaining settings.
7. Click **SAVE**.
8. Wait for the next scheduled scan to complete.

View File

@@ -2,7 +2,7 @@
"code_name": "nmap_dev_scan",
"unique_prefix": "NMAPDEV",
"plugin_type": "device_scanner",
"execution_order" : "Layer_3",
"execution_order": "Layer_3",
"enabled": true,
"data_source": "script",
"mapped_to_table": "CurrentScan",
@@ -16,7 +16,11 @@
}
],
"show_ui": true,
"localized": ["display_name", "description", "icon"],
"localized": [
"display_name",
"description",
"icon"
],
"display_name": [
{
"language_code": "en_us",
@@ -49,7 +53,11 @@
"type": {
"dataType": "string",
"elements": [
{ "elementType": "select", "elementOptions": [], "transformers": [] }
{
"elementType": "select",
"elementOptions": [],
"transformers": []
}
]
},
"default_value": "disabled",
@@ -60,8 +68,13 @@
"always_after_scan",
"on_new_device"
],
"localized": ["name", "description"],
"events": ["run"],
"localized": [
"name",
"description"
],
"events": [
"run"
],
"name": [
{
"language_code": "en_us",
@@ -98,14 +111,21 @@
"elements": [
{
"elementType": "input",
"elementOptions": [{ "readonly": "true" }],
"elementOptions": [
{
"readonly": "true"
}
],
"transformers": []
}
]
},
"default_value": "python3 /app/front/plugins/nmap_dev_scan/nmap_dev.py ",
"options": [],
"localized": ["name", "description"],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
@@ -142,14 +162,21 @@
"elements": [
{
"elementType": "input",
"elementOptions": [{ "type": "number" }],
"elementOptions": [
{
"type": "number"
}
],
"transformers": []
}
]
},
"default_value": 300,
"options": [],
"localized": ["name", "description"],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
@@ -212,7 +239,10 @@
},
"default_value": "*/5 * * * *",
"options": [],
"localized": ["name", "description"],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
@@ -249,7 +279,11 @@
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true" }],
"elementOptions": [
{
"multiple": "true"
}
],
"transformers": []
}
]
@@ -261,7 +295,10 @@
"Watched_Value3",
"Watched_Value4"
],
"localized": ["name", "description"],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
@@ -298,19 +335,28 @@
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true" }],
"elementOptions": [
{
"multiple": "true"
}
],
"transformers": []
}
]
},
"default_value": ["new"],
"default_value": [
"new"
],
"options": [
"new",
"watched-changed",
"watched-not-changed",
"missing-in-last-scan"
],
"localized": ["name", "description"],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
@@ -345,12 +391,19 @@
"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [], "transformers": [] }
{
"elementType": "input",
"elementOptions": [],
"transformers": []
}
]
},
"default_value": "sudo nmap -sn -PR -oX - ",
"options": [],
"localized": ["name", "description"],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
@@ -371,14 +424,21 @@
"elements": [
{
"elementType": "input",
"elementOptions": [{ "type": "checkbox" }],
"elementOptions": [
{
"type": "checkbox"
}
],
"transformers": []
}
]
},
"default_value": false,
"options": [],
"localized": ["name", "description"],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
@@ -401,7 +461,9 @@
"type": "none",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -417,7 +479,9 @@
"type": "device_name_mac",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -433,7 +497,9 @@
"type": "device_ip",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -449,7 +515,9 @@
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -465,7 +533,9 @@
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -489,7 +559,9 @@
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -512,7 +584,9 @@
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -535,7 +609,9 @@
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -558,7 +634,9 @@
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
@@ -598,7 +676,9 @@
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": ["name"],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",

View File

@@ -116,6 +116,9 @@ def execute_scan(subnets_list, timeout, fakeMac, args):
def execute_scan_on_interface (interface, timeout, args):
# Remove unsupported VLAN flags
interface = re.sub(r'--vlan=\S+', '', interface).strip()
# Prepare command arguments
scan_args = args.split() + interface.replace('--interface=','-e ').split()

View File

@@ -0,0 +1,64 @@
#!/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=$(printf '\033[1;35m')
RESET=$(printf '\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.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/running-as-root.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
# Set ownership to netalertx user for all read-write paths
chown -R netalertx ${READ_WRITE_PATHS} 2>/dev/null || true
# Set directory and file permissions for all read-write paths
find ${READ_WRITE_PATHS} -type d -exec chmod u+rwx {}
find ${READ_WRITE_PATHS} -type f -exec chmod u+rw {}
echo Permissions fixed for read-write paths. Please restart the container as user 20211.
sleep infinity & wait $!
fi

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
import os
import sys
from dataclasses import dataclass
# if NETALERTX_DEBUG is 1 then exit
if os.environ.get("NETALERTX_DEBUG") == "1":
sys.exit(0)
@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"
" https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/mount-configuration-issues.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
has_write_errors = 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.dataloss_risk or result.error or result.write_error or result.performance_issue:
has_issues = True
if result.write_error:
has_write_errors = True
results.append(result)
if has_issues or True: # Always print table for diagnostic purposes
# --- 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 ---
if has_issues:
print("\n", file=sys.stderr)
print_warning_message()
# Exit with error only if there are write permission issues
if has_write_errors and os.environ.get("NETALERTX_DEBUG") != "1":
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ if [ ! -f ${NETALERTX_CONFIG}/app.conf ]; then
>&2 echo "ERROR: Failed to copy default config to ${NETALERTX_CONFIG}/app.conf"
exit 2
}
RESET='\033[0m'
RESET=$(printf '\033[0m')
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
🆕 First run detected. Default configuration written to ${NETALERTX_CONFIG}/app.conf.

View File

@@ -14,8 +14,8 @@ elif [ -f "${NETALERTX_DB_FILE}" ]; then
exit 0
fi
CYAN='\033[1;36m'
RESET='\033[0m'
CYAN=$(printf '\033[1;36m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${CYAN}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
@@ -441,8 +441,8 @@ CREATE TRIGGER "trg_delete_devices"
end-of-database-schema
if [ $? -ne 0 ]; then
RED='\033[1;31m'
RESET='\033[0m'
RED=$(printf '\033[1;31m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${RED}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════

View File

@@ -0,0 +1,78 @@
#!/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=$(printf '\033[1;31m')
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\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.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/file-permissions.md
══════════════════════════════════════════════════════════════════════════════
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.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/file-permissions.md
══════════════════════════════════════════════════════════════════════════════
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).
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/file-permissions.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
fi
done
# If there were any failures, exit
if [ "$failures" -ne 0 ]; then
exit 1
fi

View File

@@ -1,7 +1,12 @@
#!/bin/sh
# check-nginx-config.sh - verify nginx conf.active mount is writable when startup needs to render config.
# check-nginx-config.sh - verify nginx conf.active mount is writable when PORT != 20211.
CONF_ACTIVE_DIR="${SYSTEM_NGINX_CONFIG}/conf.active"
# Only check nginx config writability if PORT is not the default 20211
if [ "${PORT:-20211}" = "20211" ]; then
exit 0
fi
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.
@@ -20,6 +25,8 @@ if [ ! -d "${CONF_ACTIVE_DIR}" ]; then
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.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/nginx-configuration-mount.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
@@ -40,6 +47,8 @@ if ! ( : >"${TMP_FILE}" ) 2>/dev/null; then
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 {} +
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/nginx-configuration-mount.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"

View File

@@ -36,6 +36,8 @@ RESET=$(printf '\033[0m')
* Remove any custom --user flag
* Delete "user:" overrides in compose files
* Recreate the container so volume ownership is reset
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/incorrect-user.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"

View File

@@ -1,5 +1,5 @@
#!/bin/sh
# check-network-mode.sh - detect when the container is not using host networking.
# detect when the container is not using host networking.
# Exit if NETALERTX_DEBUG=1
if [ "${NETALERTX_DEBUG}" = "1" ]; then
@@ -58,6 +58,8 @@ RESET=$(printf '\033[0m')
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.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/network-mode.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"

View File

@@ -1,5 +1,5 @@
#!/bin/sh
# check-cap.sh - Uses a real nmap command to detect missing container
# 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.
@@ -24,6 +24,8 @@ then
Without those caps, NetAlertX cannot inspect your network. Fix it before
trusting any results.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/missing-capabilities.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"

View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Bash used in this check for simplicty of math operations.
# excessive-capabilities.sh checks that no more than the necessary
# NET_ADMIN NET_BIND_SERVICE and NET_RAW capabilities are present.
# Get bounding capabilities from /proc/self/status (what can be acquired)
BND_HEX=$(grep '^CapBnd:' /proc/self/status 2>/dev/null | awk '{print $2}' | tr -d '\t')
if [ -z "$BND_HEX" ]; then
exit 0
fi
# Convert hex to decimal
BND_DEC=$(( 16#$BND_HEX )) || exit 0
# Allowed capabilities: NET_BIND_SERVICE (10), NET_ADMIN (12), NET_RAW (13)
ALLOWED_DEC=$(( ( 1 << 10 ) | ( 1 << 12 ) | ( 1 << 13 ) ))
# Check for excessive capabilities (any bits set outside allowed)
EXTRA=$(( BND_DEC & ~ALLOWED_DEC ))
if [ "$EXTRA" -ne 0 ]; then
cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ Warning: Excessive capabilities detected (bounding caps: 0x$BND_HEX).
Only NET_ADMIN, NET_BIND_SERVICE, and NET_RAW are required in this container.
Please remove unnecessary capabilities.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/excessive-capabilities.md
══════════════════════════════════════════════════════════════════════════════
EOF
fi

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# read-only-mode.sh detects and warns if running read-write on the root filesystem.
# Check if the root filesystem is mounted as read-only
if ! awk '$2 == "/" && $4 ~ /ro/ {found=1} END {exit !found}' /proc/mounts; then
cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ Warning: Container is running as read-write, not in read-only mode.
Please mount the root filesystem as --read-only or use read-only: true
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/read-only-filesystem.md
══════════════════════════════════════════════════════════════════════════════
EOF
fi

View File

@@ -0,0 +1,69 @@
#!/bin/sh
# check-ports.sh detects and warns if required ports are already in use
# or if they are configured to be the same.
# Intended for lightweight Alpine containers (uses busybox netstat).
# Define ports from ENV variables, applying defaults
PORT_APP=${PORT:-20211}
PORT_GQL=${APP_CONF_OVERRIDE:-${GRAPHQL_PORT:-20212}}
# Check if ports are configured to be the same
if [ "$PORT_APP" -eq "$PORT_GQL" ]; then
cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ Configuration Warning: Both ports are set to ${PORT_APP}.
The Application port (\$PORT) and the GraphQL API port
(\$APP_CONF_OVERRIDE or \$GRAPHQL_PORT) are configured to use the
same port. This will cause a conflict.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
══════════════════════════════════════════════════════════════════════════════
EOF
fi
# Check for netstat (usually provided by busybox)
if ! command -v netstat >/dev/null 2>&1; then
cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ Configuration Error: 'netstat' command not found.
Cannot check port availability. Please ensure 'net-tools'
or the busybox 'netstat' applet is available in this container.
══════════════════════════════════════════════════════════════════════════════
EOF
exit 0 # Exit gracefully, this is a non-fatal check
fi
# Fetch all listening TCP/UDP ports once.
# We awk $4 to get the 'Local Address' column (e.g., 0.0.0.0:20211 or :::20211)
LISTENING_PORTS=$(netstat -lntu | awk '{print $4}')
# Check Application Port
# We grep for ':{PORT}$' to match the port at the end of the string.
if echo "$LISTENING_PORTS" | grep -q ":${PORT_APP}$"; then
cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ Port Warning: Application port ${PORT_APP} is already in use.
The main application (defined by \$PORT) may fail to start.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
══════════════════════════════════════════════════════════════════════════════
EOF
fi
# Check GraphQL Port
# We add a check to avoid double-warning if ports are identical AND in use
if [ "$PORT_APP" -ne "$PORT_GQL" ] && echo "$LISTENING_PORTS" | grep -q ":${PORT_GQL}$"; then
cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ Port Warning: GraphQL API port ${PORT_GQL} is already in use.
The GraphQL API (defined by \$APP_CONF_OVERRIDE or \$GRAPHQL_PORT)
may fail to start.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
══════════════════════════════════════════════════════════════════════════════
EOF
fi

View File

@@ -38,16 +38,20 @@
################################################################################
# Banner display
printf '
\033[1;31m
RED='\033[1;31m'
RESET='\033[0m'
printf "${RED}"
echo '
_ _ _ ___ _ _ __ __
| \ | | | | / _ \| | | | \ \ / /
| \| | ___| |_/ /_\ \ | ___ _ __| |_ \ V /
| . |/ _ \ __| _ | |/ _ \ __| __|/ \
| |\ | __/ |_| | | | | __/ | | |_/ /^\ \
\_| \_/\___|\__\_| |_/_|\___|_| \__\/ \/
\033[0m
Network intruder and presence detector.
'
printf "\033[0m"
echo ' Network intruder and presence detector.
https://netalertx.com
'
@@ -55,15 +59,15 @@ set -u
FAILED_STATUS=""
echo "Startup pre-checks"
for script in ${SYSTEM_SERVICES_SCRIPTS}/check-*.sh; do
for script in ${ENTRYPOINT_CHECKS}/*; do
if [ -n "${SKIP_TESTS:-}" ]; then
echo "Skipping startup checks as SKIP_TESTS is set."
break
fi
script_name=$(basename "$script" | sed 's/^check-//;s/\.sh$//;s/-/ /g')
script_name=$(basename "$script" | sed 's/^[0-9]*-//;s/\.sh$//;s/-/ /g')
echo " --> ${script_name}"
sh "$script"
"$script"
NETALERTX_DOCKER_ERROR_CHECK=$?
if [ ${NETALERTX_DOCKER_ERROR_CHECK} -ne 0 ]; then
@@ -71,13 +75,14 @@ for script in ${SYSTEM_SERVICES_SCRIPTS}/check-*.sh; do
FAILED_STATUS="${NETALERTX_DOCKER_ERROR_CHECK}"
echo "${script_name}: FAILED with ${FAILED_STATUS}"
echo "Failure detected in: ${script}"
# Continue to next check instead of exiting immediately
fi
done
if [ -n "${FAILED_STATUS}" ]; then
echo "Container startup checks failed with exit code ${FAILED_STATUS}."
exit ${FAILED_STATUS}
# Continue with startup despite failures for testing purposes
fi
# Set APP_CONF_OVERRIDE based on GRAPHQL_PORT if not already set

View File

@@ -1,137 +0,0 @@
#!/bin/sh
# check-0-permissions.sh: Verify file system permissions for critical paths.
#
# This script ensures that the application has the necessary read and write
# permissions for its operational directories. It distinguishes between running
# as root (user 0) and a non-privileged user.
#
# As root, it will proactively fix ownership and permissions.
# As a non-root user, it will only warn about issues.
# --- Color Codes ---
RED='\033[1;31m'
YELLOW='\033[1;33m'
MAGENTA='\033[1;35m'
RESET='\033[0m'
# --- Main Logic ---
# Define paths that need read-only access
READ_ONLY_PATHS="
${NETALERTX_APP}
${NETALERTX_SERVER}
${NETALERTX_FRONT}
${SYSTEM_SERVICES_CONFIG}
${VIRTUAL_ENV}
"
# 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
# --- Permission Validation ---
failures=0
# Check all paths
ALL_PATHS="${READ_ONLY_PATHS} ${READ_WRITE_PATHS}"
echo "${READ_ONLY_PATHS}" | while IFS= read -r path; do
[ -z "$path" ] && continue
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}"
fi
done
# Check read-write paths specifically for write access
for path in $READ_WRITE_PATHS; do
if [ -e "$path" ] && [ ! -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

@@ -1,45 +0,0 @@
#!/bin/sh
# check-storage-extra.sh - ensure additional NetAlertX directories are persistent mounts.
if [ "${NETALERTX_DEBUG}" == "1" ]; then
exit 0
fi
warn_if_not_persistent_mount() {
path="$1"
label="$2"
if awk -v target="${path}" '$5 == target {found=1} END {exit found ? 0 : 1}' /proc/self/mountinfo; then
return 0
fi
failures=1
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: ${path} is not a persistent mount.
${label} relies on host storage to persist data across container restarts.
Mount this directory from the host or a named volume before trusting the
container's output.
Example:
--mount type=bind,src=/path/on/host,dst=${path}
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
return 1
}
failures=0
warn_if_not_persistent_mount "${NETALERTX_LOG}" "Logs" || failures=$((failures + 1))
warn_if_not_persistent_mount "${NETALERTX_API}" "API JSON cache" || failures=$((failures + 1))
warn_if_not_persistent_mount "${SYSTEM_SERVICES_RUN}" "Runtime work directory" || failures=$((failures + 1))
if [ "${failures}" -ne 0 ]; then
exit 1
fi
exit 0

View File

@@ -1,84 +0,0 @@
#!/bin/sh
# check-storage.sh - Verify critical paths are persistent mounts.
# Define non-persistent filesystem types to check against
# NOTE: 'overlay' and 'aufs' are the primary non-persistent types for container roots.
# 'tmpfs' and 'ramfs' are for specific non-persistent mounts.
NON_PERSISTENT_FSTYPES="tmpfs|ramfs|overlay|aufs"
MANDATORY_PERSISTENT_PATHS="/app/db /app/config"
# This function is now the robust persistence checker.
is_persistent_mount() {
target_path="$1"
mount_entry=$(awk -v path="${target_path}" '$2 == path { print $0 }' /proc/mounts)
if [ -z "${mount_entry}" ]; then
# CRITICAL FIX: If the mount entry is empty, check if it's one of the mandatory paths.
if echo "${MANDATORY_PERSISTENT_PATHS}" | grep -w -q "${target_path}"; then
# The path is mandatory but not mounted: FAIL (Not persistent)
return 1
else
# Not mandatory and not a mount point: Assume persistence is inherited from parent (pass)
return 0
fi
fi
# ... (rest of the original logic remains the same for explicit mounts)
fs_type=$(echo "${mount_entry}" | awk '{print $3}')
# Check if the filesystem type matches any non-persistent types
if echo "${fs_type}" | grep -E -q "^(${NON_PERSISTENT_FSTYPES})$"; then
return 1 # Not persistent (matched a non-persistent type)
else
return 0 # Persistent
fi
}
warn_if_not_persistent_mount() {
path="$1"
if is_persistent_mount "${path}"; then
return 0
fi
failures=1
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: ${path} is not a persistent mount.
Your data in this directory may not persist across container restarts or
upgrades. The filesystem type for this path is identified as non-persistent.
Fix: mount ${path} explicitly as a bind mount or a named volume:
# Bind mount
--mount type=bind,src=/path/on/host,dst=${path}
# Named volume
--mount type=volume,src=netalertx-data,dst=${path}
Apply one of these mount options and restart the container.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
}
# If NETALERTX_DEBUG=1 then we will exit
if [ "${NETALERTX_DEBUG}" = "1" ]; then
exit 0
fi
failures=0
# NETALERTX_DB is a file, so we check its directory
warn_if_not_persistent_mount "$(dirname "${NETALERTX_DB_FILE}")"
warn_if_not_persistent_mount "${NETALERTX_CONFIG}"
if [ "${failures}" -ne 0 ]; then
# We only warn, not exit, as this is not a critical failure
# but the user should be aware of the potential data loss.
sleep 1 # Give user time to read the message
fi

View File

@@ -1,48 +0,0 @@
#!/bin/sh
# storage-check.sh - Verify critical paths use dedicated mounts.
warn_if_not_dedicated_mount() {
path="$1"
if awk -v target="${path}" '$5 == target {found=1} END {exit found ? 0 : 1}' /proc/self/mountinfo; then
return 0
fi
failures=1
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: ${path} is not mounted separately inside this container.
NetAlertX runs as a single unprivileged process and pounds this directory
with writes. Leaving it on the container overlay will thrash storage and
slow the stack.
Fix: mount ${path} explicitly — tmpfs for ephemeral data, or bind/volume if
you want to preserve history:
--mount type=tmpfs,destination=${path}
# or
--mount type=bind,src=/path/on/host,dst=${path}
Apply the mount and restart the container.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
}
# If NETALERTX_DEBUG=1 then we will exit
if [ "${NETALERTX_DEBUG}" = "1" ]; then
exit 0
fi
failures=0
warn_if_not_dedicated_mount "${NETALERTX_API}"
warn_if_not_dedicated_mount "${NETALERTX_LOG}"
if [ ! -w "${SYSTEM_NGINX_CONFIG}/conf.active" ]; then
echo "Note: Using default listen address 0.0.0.0:20211 instead of ${LISTEN_ADDR}:${PORT} (no ${SYSTEM_NGINX_CONFIG}/conf.active override)."
fi
exit 0

View File

@@ -1,35 +0,0 @@
#!/bin/sh
# check-root.sh - ensure the container is not running as root.
CURRENT_UID="$(id -u)"
if [ "${CURRENT_UID}" -eq 0 ]; then
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<'EOF'
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: NetAlertX is running as root (UID 0).
This defeats every hardening safeguard built into the image. You just
handed a high-value network monitoring appliance full control over your
host. If an attacker compromises NetAlertX now, the entire machine goes
with it.
Run the container as the dedicated 'netalertx' user instead:
* Keep the default USER in the image (20211:20211), or
* In docker-compose.yml, remove any 'user:' override that sets UID 0.
Note: As a courtesy, this special mode is only used to set the permissions
of /app/db and /app/config to be owned by the netalertx user so future
runs work correctly.
Bottom line: never run security tooling as root unless you are actively
trying to get pwned.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 1
fi
exit 0

View File

@@ -35,7 +35,6 @@ from database import DB
from messaging.reporting import get_notifications
from models.notification_instance import NotificationInstance
from models.user_events_queue_instance import UserEventsQueueInstance
from plugin import plugin_manager
from scan.device_handling import update_devices_names
from workflows.manager import WorkflowManager
@@ -152,16 +151,19 @@ def main ():
process_scan(db)
updateState("Scan processed", None, None, None, None, False)
# ------------------------------------------------------------------------------
# Reporting
# ------------------------------------------------------------------------------
# Name resolution
# --------------------------------------------
# run plugins before notification processing (e.g. Plugins to discover device names)
pm.run_plugin_scripts('before_name_updates')
# Resolve devices names
mylog('debug','[Main] Resolve devices names')
update_devices_names(db)
update_devices_names(pm)
# --------
# Reporting
# Check if new devices found
sql.execute (sql_new_devices)

View File

@@ -16,7 +16,44 @@ INSTALL_PATH="/app"
# A class to manage the application state and to provide a frontend accessible API point
# To keep an existing value pass None
class app_state_class:
def __init__(self, currentState = None, settingsSaved=None, settingsImported=None, showSpinner=False, graphQLServerStarted=0, processScan=False):
"""
Represents the current state of the application for frontend communication.
Attributes:
lastUpdated (str): Timestamp of the last update.
settingsSaved (int): Flag indicating if settings were saved.
settingsImported (int): Flag indicating if settings were imported.
showSpinner (bool): Whether the UI spinner should be shown.
processScan (bool): Whether a scan process is active.
graphQLServerStarted (int): Timestamp of GraphQL server start.
currentState (str): Current state string.
pluginsStates (dict): Per-plugin state information.
isNewVersion (bool): Flag indicating if a new version is available.
isNewVersionChecked (int): Timestamp of last version check.
"""
def __init__(self, currentState=None,
settingsSaved=None,
settingsImported=None,
showSpinner=None,
graphQLServerStarted=0,
processScan=False,
pluginsStates=None):
"""
Initialize the application state, optionally overwriting previous values.
Loads previous state from 'app_state.json' if available, otherwise initializes defaults.
New values provided via parameters overwrite previous state.
Args:
currentState (str, optional): Initial current state.
settingsSaved (int, optional): Initial settingsSaved flag.
settingsImported (int, optional): Initial settingsImported flag.
showSpinner (bool, optional): Initial showSpinner flag.
graphQLServerStarted (int, optional): Initial GraphQL server timestamp.
processScan (bool, optional): Initial processScan flag.
pluginsStates (dict, optional): Initial plugin states to merge with previous state.
"""
# json file containing the state to communicate with the frontend
stateFile = apiPath + 'app_state.json'
previousState = ""
@@ -27,7 +64,7 @@ class app_state_class:
if os.path.exists(stateFile):
try:
with open(stateFile, 'r') as json_file:
previousState = json.load(json_file)
previousState = json.load(json_file)
except json.decoder.JSONDecodeError as e:
mylog('none', [f'[app_state_class] Failed to handle app_state.json: {e}'])
@@ -41,6 +78,7 @@ class app_state_class:
self.isNewVersionChecked = previousState.get("isNewVersionChecked", 0)
self.graphQLServerStarted = previousState.get("graphQLServerStarted", 0)
self.currentState = previousState.get("currentState", "Init")
self.pluginsStates = previousState.get("pluginsStates", {})
else: # init first time values
self.settingsSaved = 0
self.settingsImported = 0
@@ -50,6 +88,7 @@ class app_state_class:
self.isNewVersionChecked = int(timeNow().timestamp())
self.graphQLServerStarted = 0
self.currentState = "Init"
self.pluginsStates = {}
# Overwrite with provided parameters if supplied
if settingsSaved is not None:
@@ -64,6 +103,20 @@ class app_state_class:
self.processScan = processScan
if currentState is not None:
self.currentState = currentState
# Merge plugin states instead of overwriting
if pluginsStates is not None:
for plugin, state in pluginsStates.items():
if plugin in self.pluginsStates:
# Only update existing keys if both are dicts
if isinstance(self.pluginsStates[plugin], dict) and isinstance(state, dict):
self.pluginsStates[plugin].update(state)
else:
# Replace if types don't match
self.pluginsStates[plugin] = state
else:
# Optionally ignore or add new plugin entries
# To ignore new plugins, comment out the next line
self.pluginsStates[plugin] = state
# check for new version every hour and if currently not running new version
if self.isNewVersion is False and self.isNewVersionChecked + 3600 < int(timeNow().timestamp()):
@@ -74,7 +127,7 @@ class app_state_class:
# with open(stateFile, 'w') as json_file:
# json.dump(self, json_file, cls=AppStateEncoder, indent=4)
# Remove lastUpdated from the dictionary for comparison
# Remove lastUpdated from the dictionary for comparison
currentStateDict = self.__dict__.copy()
currentStateDict.pop('lastUpdated', None)
@@ -88,20 +141,51 @@ class app_state_class:
except (TypeError, ValueError) as e:
mylog('none', [f'[app_state_class] Failed to serialize object to JSON: {e}'])
return # Allows chaining by returning self
return
#-------------------------------------------------------------------------------
# method to update the state
def updateState(newState = None, settingsSaved = None, settingsImported = None, showSpinner = False, graphQLServerStarted = None, processScan = None):
def updateState(newState = None,
settingsSaved = None,
settingsImported = None,
showSpinner = None,
graphQLServerStarted = None,
processScan = None,
pluginsStates=None):
"""
Convenience method to create or update the app state.
return app_state_class(newState, settingsSaved, settingsImported, showSpinner, graphQLServerStarted, processScan)
Args:
newState (str, optional): Current state to set.
settingsSaved (int, optional): Flag for settings saved.
settingsImported (int, optional): Flag for settings imported.
showSpinner (bool, optional): Flag to control UI spinner.
graphQLServerStarted (int, optional): Timestamp of GraphQL server start.
processScan (bool, optional): Flag indicating if a scan is active.
pluginsStates (dict, optional): Plugin state updates.
Returns:
app_state_class: Updated state object.
"""
return app_state_class( newState,
settingsSaved,
settingsImported,
showSpinner,
graphQLServerStarted,
processScan,
pluginsStates)
#-------------------------------------------------------------------------------
# Checks if the object has a __dict__ attribute. If it does, it assumes that it's an instance of a class and serializes its attributes dynamically.
class AppStateEncoder(json.JSONEncoder):
"""
JSON encoder for application state objects.
Automatically serializes objects with a __dict__ attribute.
"""
def default(self, obj):
if hasattr(obj, '__dict__'):
# If the object has a '__dict__', assume it's an instance of a class

View File

@@ -195,7 +195,10 @@ def ensure_Indexes(sql) -> bool:
("idx_dev_location", "CREATE INDEX idx_dev_location ON Devices(devLocation)"),
# Settings
("idx_set_key", "CREATE INDEX idx_set_key ON Settings(setKey)")
("idx_set_key", "CREATE INDEX idx_set_key ON Settings(setKey)"),
# Plugins_Objects
("idx_plugins_plugin_mac_ip", "CREATE INDEX idx_plugins_plugin_mac_ip ON Plugins_Objects(Plugin, Object_PrimaryID, Object_SecondaryID)") # Issue #1251: Optimize name resolution lookup
]
for name, create_sql in indexes:

View File

@@ -449,8 +449,8 @@ def read_config_file(filename):
#-------------------------------------------------------------------------------
# DEPERECATED soonest after 10/10/2024
# 🤔Idea/TODO: Check and compare versions/timestamps amd only perform a replacement if config/version older than...
# DEPRECATE soonest after 10/10/2024
# 🤔Idea/TODO: Check and compare versions/timestamps and only perform a replacement if config/version older than...
replacements = {
r'\bREPORT_TO\b': 'SMTP_REPORT_TO',
r'\bSYNC_api_token\b': 'API_TOKEN',

View File

@@ -26,6 +26,8 @@ class plugin_manager:
def __init__(self, db, all_plugins):
self.db = db
self.all_plugins = all_plugins
self.plugin_states = {}
self.name_plugins_checked = None
# object cache of settings and schedules for faster lookups
self._cache = {}
@@ -66,20 +68,6 @@ class plugin_manager:
# 🔹 Lookup RUN setting from cache instead of calling get_plugin_setting_obj each time
run_setting = self._cache["settings"].get(prefix, {}).get("RUN")
# set = get_plugin_setting_obj(plugin, "RUN")
# mylog('debug', [f'[run_plugin_scripts] plugin: {plugin}'])
# mylog('debug', [f'[run_plugin_scripts] set: {set}'])
# if set != None and set['value'] == runType:
# if runType != "schedule":
# shouldRun = True
# elif runType == "schedule":
# # run if overdue scheduled time
# # check schedules if any contains a unique plugin prefix matching the current plugin
# for schd in conf.mySchedules:
# if schd.service == prefix:
# # Check if schedule overdue
# shouldRun = schd.runScheduleCheck()
if run_setting != None and run_setting['value'] == runType:
if runType != "schedule":
shouldRun = True
@@ -91,19 +79,6 @@ class plugin_manager:
# Check if schedule overdue
shouldRun = schd.runScheduleCheck()
# if shouldRun:
# # Header
# updateState(f"Plugin: {prefix}")
# print_plugin_info(plugin, ['display_name'])
# mylog('debug', ['[Plugins] CMD: ', get_plugin_setting_obj(plugin, "CMD")["value"]])
# execute_plugin(self.db, self.all_plugins, plugin)
# # update last run time
# if runType == "schedule":
# for schd in conf.mySchedules:
# if schd.service == prefix:
# # note the last time the scheduled plugin run was executed
# schd.last_run = timeNowTZ()
if shouldRun:
# Header
updateState(f"Plugin: {prefix}")
@@ -116,6 +91,10 @@ class plugin_manager:
execute_plugin(self.db, self.all_plugins, plugin)
# Update plugin states in app_state
current_plugin_state = self.get_plugin_states(prefix) # get latest plugin state
updateState(pluginsStates={prefix: current_plugin_state.get(prefix, {})})
# update last run time
if runType == "schedule":
schd = self._cache["schedules"].get(prefix)
@@ -183,12 +162,20 @@ class plugin_manager:
mylog('minimal', ['[', timeNowTZ(), '] START Run: ', runType])
# run the plugin to run
# run the plugin
for plugin in self.all_plugins:
if plugin["unique_prefix"] == runType:
pluginName = plugin["unique_prefix"]
execute_plugin(self.db, self.all_plugins, plugin)
# Update plugin states in app_state
current_plugin_state = self.get_plugin_states(pluginName) # get latest plugin state
updateState(pluginsStates={pluginName: current_plugin_state.get(pluginName, {})})
mylog('minimal', ['[', timeNowTZ(), '] END Run: ', runType])
return
@@ -215,6 +202,76 @@ class plugin_manager:
return
#-------------------------------------------------------------------------------
def get_plugin_states(self, plugin_name=None):
"""
Returns plugin state summary suitable for updateState(..., pluginsStates=...).
If plugin_name is provided, only calculates stats for that plugin.
Structure per plugin:
{
"lastChanged": str,
"totalObjects": int,
"newObjects": int,
"changedObjects": int,
"stateUpdated": str
}
"""
sql = self.db.sql
plugin_states = {}
if plugin_name: # Only compute for single plugin
sql.execute("""
SELECT MAX(DateTimeChanged) AS last_changed,
COUNT(*) AS total_objects,
SUM(CASE WHEN DateTimeCreated = DateTimeChanged THEN 1 ELSE 0 END) AS new_objects,
CURRENT_TIMESTAMP AS state_updated
FROM Plugins_Objects
WHERE Plugin = ?
""", (plugin_name,))
row = sql.fetchone()
last_changed, total_objects, new_objects, state_updated = row if row else ("", 0, 0, "")
new_objects = new_objects or 0 # ensure it's int
changed_objects = total_objects - new_objects
plugin_states[plugin_name] = {
"lastChanged": last_changed or "",
"totalObjects": total_objects or 0,
"newObjects": new_objects or 0,
"changedObjects": changed_objects or 0,
"stateUpdated": state_updated or ""
}
# Save in memory
self.plugin_states[plugin_name] = plugin_states[plugin_name]
else: # Compute for all plugins (full refresh)
sql.execute("""
SELECT Plugin,
MAX(DateTimeChanged) AS last_changed,
COUNT(*) AS total_objects,
SUM(CASE WHEN DateTimeCreated = DateTimeChanged THEN 1 ELSE 0 END) AS new_objects,
CURRENT_TIMESTAMP AS state_updated
FROM Plugins_Objects
GROUP BY Plugin
""")
for plugin, last_changed, total_objects, new_objects, state_updated in sql.fetchall():
new_objects = new_objects or 0 # ensure it's int
changed_objects = total_objects - new_objects
plugin_states[plugin] = {
"lastChanged": last_changed or "",
"totalObjects": total_objects or 0,
"newObjects": new_objects or 0,
"changedObjects": changed_objects or 0,
"stateUpdated": state_updated or ""
}
# Save in memory
self.plugin_states = plugin_states
return plugin_states
#-------------------------------------------------------------------------------
class plugin_param:

View File

@@ -3,19 +3,23 @@ import subprocess
import conf
import os
import re
from dateutil import parser
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value, check_IP_format
from logger import mylog
from logger import mylog, Logger
from const import vendorsPath, vendorsPathNewest, sql_generateGuid
from models.device_instance import DeviceInstance
from scan.name_resolution import NameResolver
from scan.device_heuristics import guess_icon, guess_type
from db.db_helper import sanitize_SQL_input, list_to_where
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
#-------------------------------------------------------------------------------
# Removing devices from the CurrentScan DB table which the user chose to ignore by MAC or IP
def exclude_ignored_devices(db):
@@ -516,19 +520,42 @@ def create_new_devices (db):
#-------------------------------------------------------------------------------
def update_devices_names(db):
sql = db.sql
resolver = NameResolver(db)
device_handler = DeviceInstance(db)
def update_devices_names(pm):
sql = pm.db.sql
resolver = NameResolver(pm.db)
device_handler = DeviceInstance(pm.db)
# --- Short-circuit if no name-resolution plugin has changed ---
name_plugins = ["DIGSCAN", "NSLOOKUP", "NBTSCAN", "AVAHISCAN"]
# Retrieve last time name resolution was checked (string or datetime)
last_checked_str = pm.name_plugins_checked
last_checked_dt = parser.parse(last_checked_str) if isinstance(last_checked_str, str) else last_checked_str
# Collect valid state update timestamps for name-related plugins
state_times = []
for p in name_plugins:
state_updated = pm.plugin_states.get(p, {}).get("stateUpdated")
if state_updated and state_updated.strip(): # skip empty or None
state_times.append(state_updated)
# Determine the latest valid stateUpdated timestamp
latest_state_str = max(state_times, default=None)
latest_state_dt = parser.parse(latest_state_str) if latest_state_str else None
# Skip if no plugin state changed since last check
if last_checked_dt and latest_state_dt and latest_state_dt <= last_checked_dt:
mylog('debug', '[Update Device Name] No relevant name plugin changes since last check — skipping update.')
return
nameNotFound = "(name not found)"
# Define resolution strategies in priority order
strategies = [
(resolver.resolve_dig, 'dig'),
(resolver.resolve_mdns, 'mdns'),
(resolver.resolve_nslookup, 'nslookup'),
(resolver.resolve_nbtlookup, 'nbtlookup')
(resolver.resolve_dig, 'DIGSCAN'),
(resolver.resolve_mdns, 'AVAHISCAN'),
(resolver.resolve_nslookup, 'NSLOOKUP'),
(resolver.resolve_nbtlookup, 'NBTSCAN')
]
def resolve_devices(devices, resolve_both_name_and_fqdn=True):
@@ -590,7 +617,7 @@ def update_devices_names(db):
recordsToUpdate, recordsNotFound, foundStats, notFound = resolve_devices(unknownDevices)
# Log summary
mylog('verbose', f"[Update Device Name] Names Found (DiG/mDNS/NSLOOKUP/NBTSCAN): {len(recordsToUpdate)} ({foundStats['dig']}/{foundStats['mdns']}/{foundStats['nslookup']}/{foundStats['nbtlookup']})")
mylog('verbose', f"[Update Device Name] Names Found (DIGSCAN/AVAHISCAN/NSLOOKUP/NBTSCAN): {len(recordsToUpdate)} ({foundStats['DIGSCAN']}/{foundStats['AVAHISCAN']}/{foundStats['NSLOOKUP']}/{foundStats['NBTSCAN']})")
mylog('verbose', f'[Update Device Name] Names Not Found : {notFound}')
# Apply updates to database
@@ -607,14 +634,21 @@ def update_devices_names(db):
recordsToUpdate, _, foundStats, notFound = resolve_devices(allDevices, resolve_both_name_and_fqdn=False)
# Log summary
mylog('verbose', f"[Update FQDN] Names Found (DiG/mDNS/NSLOOKUP/NBTSCAN): {len(recordsToUpdate)} ({foundStats['dig']}/{foundStats['mdns']}/{foundStats['nslookup']}/{foundStats['nbtlookup']})")
mylog('verbose', f"[Update FQDN] Names Found (DIGSCAN/AVAHISCAN/NSLOOKUP/NBTSCAN): {len(recordsToUpdate)} ({foundStats['DIGSCAN']}/{foundStats['AVAHISCAN']}/{foundStats['NSLOOKUP']}/{foundStats['NBTSCAN']})")
mylog('verbose', f'[Update FQDN] Names Not Found : {notFound}')
# Apply FQDN-only updates
sql.executemany("UPDATE Devices SET devFQDN = ? WHERE devMac = ?", recordsToUpdate)
# Commit all database changes
db.commitDB()
pm.db.commitDB()
# --- Step 3: Log last checked time ---
# After resolving names, update last checked
sql = pm.db.sql
sql.execute("SELECT CURRENT_TIMESTAMP")
row = sql.fetchone()
pm.name_plugins_checked = row[0] if row else None
#-------------------------------------------------------------------------------
# Updates devPresentLastScan for parent devices based on the presence of their NICs

View File

@@ -35,7 +35,7 @@ class NameResolver:
WHERE Plugin = '{plugin}' AND Object_PrimaryID = '{pMAC}'
""")
result = sql.fetchall()
self.db.commitDB()
# self.db.commitDB() # Issue #1251: Optimize name resolution lookup
if result:
raw = result[0][0]
return ResolvedName(raw, self.clean_device_name(raw, False))
@@ -46,7 +46,7 @@ class NameResolver:
WHERE Plugin = '{plugin}' AND Object_SecondaryID = '{pIP}'
""")
result = sql.fetchall()
self.db.commitDB()
# self.db.commitDB() # Issue #1251: Optimize name resolution lookup
if result:
raw = result[0][0]
return ResolvedName(raw, self.clean_device_name(raw, True))

View File

@@ -0,0 +1,46 @@
# NetAlertX Docker Test Configurations
This directory contains docker-compose configurations for different test scenarios.
## Available Configurations
### readonly
- **File**: `docker-compose.readonly.yml`
- **Description**: Tests with a read-only container filesystem
- **Use case**: Verify that the application works correctly when the container filesystem is read-only
### writable
- **File**: `docker-compose.writable.yml`
- **Description**: Tests with writable tmpfs mounts for performance
- **Use case**: Standard testing with optimized writable directories
## Mount Diagnostic Tests
The `mount-tests/` subdirectory contains 24 docker-compose configurations that test all possible mount scenarios for each path that NetAlertX monitors:
- **6 paths**: `/app/db`, `/app/config`, `/app/api`, `/app/log`, `/services/run`, `/services/config/nginx/conf.active`
- **4 scenarios per path**: `no-mount`, `ramdisk`, `mounted`, `unwritable`
- **Total**: 24 comprehensive test configurations
### Running Tests
Use pytest to run the mount diagnostic tests:
```bash
cd /workspaces/NetAlertX/test/docker_tests
pytest test_mount_diagnostics_pytest.py -v
```
Or run specific test scenarios:
```bash
pytest test_mount_diagnostics_pytest.py -k "db_ramdisk"
```
### Test Coverage
Each test validates that the mount diagnostic tool (`/entrypoint.d/10-mounts.py`) correctly identifies:
- **Good configurations**: No issues reported, exit code 0
- **Bad configurations**: Issues detected in table format, exit code 1
The tests ensure that persistent paths (db, config) require durable storage (volumes) while non-persistent paths (api, log, run) benefit from fast storage (tmpfs).

View File

@@ -0,0 +1,49 @@
services:
netalertx:
# Missing capabilities configuration for testing
network_mode: ${NETALERTX_NETWORK_MODE:-host}
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-missing-caps
read_only: true
cap_drop:
- ALL # Drop all capabilities to test missing capabilities scenario
volumes:
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
environment:
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0}
PORT: ${PORT:-20211}
APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212}
ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false}
NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0}
mem_limit: 2048m
mem_reservation: 1024m
cpu_shares: 512
pids_limit: 512
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
netalertx_config:
netalertx_db:

View File

@@ -0,0 +1,55 @@
services:
netalertx:
# Read-only container configuration for testing
network_mode: ${NETALERTX_NETWORK_MODE:-host}
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-readonly
read_only: true
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
volumes:
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
environment:
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0}
PORT: ${PORT:-20211}
APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212}
ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false}
NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0}
mem_limit: 2048m
mem_reservation: 1024m
cpu_shares: 512
pids_limit: 512
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped
volumes:
netalertx_config:
netalertx_db:

View File

@@ -0,0 +1,68 @@
services:
netalertx:
# Writable container configuration with tmpfs mounts for performance testing
network_mode: ${NETALERTX_NETWORK_MODE:-host}
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-writable
read_only: false
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
volumes:
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
# Tempfs mounts for writable directories in a read-only container and improve system performance
tmpfs:
# Speed up logging
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# Speed up API access
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,sync,noatime,nodiratime"
# Required for customization of the nginx listen addr/port
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# Required for nginx and php
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# Required by php for session save
- "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
environment:
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0}
PORT: ${PORT:-20211}
APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212}
ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false}
NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0}
mem_limit: 2048m
mem_reservation: 1024m
cpu_shares: 512
pids_limit: 512
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped
volumes:
netalertx_config:
netalertx_db:

View File

@@ -0,0 +1,45 @@
# Mount Diagnostic Test Configurations
This directory contains docker-compose files for testing all possible mount configurations.
## Generated Files
- `docker-compose.mount-test.db_no-mount.yml`: No mount - use container filesystem for db_no-mount
- `docker-compose.mount-test.db_ramdisk.yml`: RAM disk (tmpfs) for db_ramdisk
- `docker-compose.mount-test.db_mounted.yml`: Proper mount (volume for persistent, none for non-persistent) for db_mounted
- `docker-compose.mount-test.db_unwritable.yml`: Read-only mount for db_unwritable
- `docker-compose.mount-test.config_no-mount.yml`: No mount - use container filesystem for config_no-mount
- `docker-compose.mount-test.config_ramdisk.yml`: RAM disk (tmpfs) for config_ramdisk
- `docker-compose.mount-test.config_mounted.yml`: Proper mount (volume for persistent, none for non-persistent) for config_mounted
- `docker-compose.mount-test.config_unwritable.yml`: Read-only mount for config_unwritable
- `docker-compose.mount-test.api_no-mount.yml`: No mount - use container filesystem for api_no-mount
- `docker-compose.mount-test.api_ramdisk.yml`: RAM disk (tmpfs) for api_ramdisk
- `docker-compose.mount-test.api_mounted.yml`: Proper mount (volume for persistent, none for non-persistent) for api_mounted
- `docker-compose.mount-test.api_unwritable.yml`: Read-only mount for api_unwritable
- `docker-compose.mount-test.log_no-mount.yml`: No mount - use container filesystem for log_no-mount
- `docker-compose.mount-test.log_ramdisk.yml`: RAM disk (tmpfs) for log_ramdisk
- `docker-compose.mount-test.log_mounted.yml`: Proper mount (volume for persistent, none for non-persistent) for log_mounted
- `docker-compose.mount-test.log_unwritable.yml`: Read-only mount for log_unwritable
- `docker-compose.mount-test.run_no-mount.yml`: No mount - use container filesystem for run_no-mount
- `docker-compose.mount-test.run_ramdisk.yml`: RAM disk (tmpfs) for run_ramdisk
- `docker-compose.mount-test.run_mounted.yml`: Proper mount (volume for persistent, none for non-persistent) for run_mounted
- `docker-compose.mount-test.run_unwritable.yml`: Read-only mount for run_unwritable
- `docker-compose.mount-test.active_config_no-mount.yml`: No mount - use container filesystem for active_config_no-mount
- `docker-compose.mount-test.active_config_ramdisk.yml`: RAM disk (tmpfs) for active_config_ramdisk
- `docker-compose.mount-test.active_config_mounted.yml`: Proper mount (volume for persistent, none for non-persistent) for active_config_mounted
- `docker-compose.mount-test.active_config_unwritable.yml`: Read-only mount for active_config_unwritable
## Usage
Run tests using pytest:
```bash
cd /workspaces/NetAlertX/test/docker_tests
pytest test_mount_diagnostics_pytest.py
```
Or run specific scenarios:
```bash
pytest test_mount_diagnostics_pytest.py -k "db_ramdisk"
```

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container starts successfully with proper nginx config mount
# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as writable and mounted
# - No configuration warnings for nginx config path
# - Custom PORT configuration should work when nginx config is writable
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-active_config_mounted
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_ACTIVE_CONFIG: /services/config/nginx/conf.active
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_system_services_active_config
target: /services/config/nginx/conf.active
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,48 @@
# Expected outcome: Container shows warning about missing nginx config mount
# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as not mounted
# - Warning message about nginx configuration mount being missing
# - Custom PORT configuration may not work properly
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-active_config_no-mount
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_ACTIVE_CONFIG: /services/config/nginx/conf.active
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container shows performance warning for nginx config on RAM disk
# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as mounted on tmpfs (RAM disk)
# - Performance issue warning since nginx config should be persistent
# - Custom PORT configuration may have performance implications
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-active_config_ramdisk
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_ACTIVE_CONFIG: /services/config/nginx/conf.active
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container fails to start due to unwritable nginx config partition
# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as mounted but unwritable (❌ in Writeable column)
# - 35-nginx-config.sh detects permission error and exits with code 1
# - Container startup fails because nginx configuration cannot be written for custom ports
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-active_config_unwritable
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_ACTIVE_CONFIG: /services/config/nginx/conf.active
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_system_services_active_config
target: /services/config/nginx/conf.active
read_only: true
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container starts successfully with proper API mount
# - NETALERTX_API shows as writable and mounted
# - No configuration warnings for API path
# - API data persistence works correctly
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-api_mounted
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_API: /app/api
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_netalertx_api
target: /app/api
read_only: false
tmpfs:
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,48 @@
# Expected outcome: Container shows mount error for API directory
# - NETALERTX_API shows as not mounted
# - Mount error since API directory should be mounted for proper operation
# - API functionality may be limited
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-api_no-mount
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_API: /app/api
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container shows performance warning for API on RAM disk
# - NETALERTX_API shows as mounted on tmpfs (RAM disk)
# - Performance issue warning since API data should be on persistent storage
# - API data will be lost on container restart
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-api_ramdisk
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_API: /app/api
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container fails to start due to unwritable API partition
# - NETALERTX_API shows as mounted but unwritable (❌ in Writeable column)
# - API directory must be writable for proper operation
# - Container startup fails because API functionality cannot work without write access
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-api_unwritable
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_API: /app/api
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_netalertx_api
target: /app/api
read_only: true
tmpfs:
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container starts successfully with proper config mount
# - NETALERTX_CONFIG shows as writable and mounted
# - No configuration warnings for config path
# - Configuration persistence works correctly
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-config_mounted
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_CONFIG: /app/config
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: test_netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,45 @@
# Expected outcome: Container shows mount error for config directory
# - NETALERTX_CONFIG shows as not mounted
# - Mount error since config directory should be mounted for proper operation
# - Configuration may not persist across restarts
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-config_no-mount
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_CONFIG: /app/config
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,46 @@
# Expected outcome: Container shows dataloss risk warning for config on RAM disk
# - NETALERTX_CONFIG shows as mounted on tmpfs (RAM disk)
# - Dataloss risk warning since config data should be persistent
# - Configuration will be lost on container restart
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-config_ramdisk
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_CONFIG: /app/config
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
tmpfs:
- "/app/config:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container fails to start due to unwritable config partition
# - NETALERTX_CONFIG shows as mounted but unwritable (❌ in Writeable column)
# - 30-writable-config.sh detects permission error and exits with code 1
# - Container startup fails because config files cannot be written to
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-config_unwritable
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_CONFIG: /app/config
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: test_netalertx_config
target: /app/config
read_only: true
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container starts successfully with proper database mount
# - NETALERTX_DB shows as writable and mounted
# - No configuration warnings for database path
# - Database persistence works correctly
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-db_mounted
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_DB: /app/db
volumes:
- type: volume
source: test_netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,45 @@
# Expected outcome: Container shows mount error warning but continues running
# - NETALERTX_DB shows as not mounted (❌ in Mount column) but path gets created
# - Warning message displayed about configuration issues
# - Container continues because database directory can be created in writable filesystem
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-db_no-mount
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_DB: /app/db
volumes:
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,46 @@
# Expected outcome: Container shows dataloss risk warning for database on RAM disk
# - NETALERTX_DB shows as mounted on tmpfs (RAM disk)
# - Dataloss risk warning since database should be persistent
# - Database will be lost on container restart
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-db_ramdisk
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_DB: /app/db
volumes:
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/db:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container fails to start due to unwritable database partition
# - NETALERTX_DB shows as mounted but unwritable (❌ in Writeable column)
# - 30-writable-config.sh detects permission error and exits with code 1
# - Container startup fails because database files cannot be written to
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-db_unwritable
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_DB: /app/db
volumes:
- type: volume
source: test_netalertx_db
target: /app/db
read_only: true
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container starts successfully with proper log mount
# - NETALERTX_LOG shows as mounted and writable
# - No mount warnings since logs can be non-persistent
# - Container starts normally with logging enabled
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-log_mounted
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_LOG: /app/log
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_netalertx_log
target: /app/log
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,48 @@
# Expected outcome: Container shows mount error warning but continues running
# - NETALERTX_LOG shows as not mounted (❌ in Mount column)
# - Warning message displayed about configuration issues
# - Container continues to run despite the mount error
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-log_no-mount
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_LOG: /app/log
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container shows dataloss risk warning for logs on RAM disk
# - NETALERTX_LOG shows as mounted on tmpfs (RAM disk)
# - Dataloss risk warning since logs may be lost on restart
# - Container starts but logs may not persist
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-log_ramdisk
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_LOG: /app/log
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container fails to start due to unwritable log partition
# - NETALERTX_LOG shows as mounted but unwritable (❌ in Writeable column)
# - 25-mandatory-folders.sh cannot create required log files and fails
# - Container startup fails because logging infrastructure cannot be initialized
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-log_unwritable
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
NETALERTX_LOG: /app/log
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_netalertx_log
target: /app/log
read_only: true
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container starts successfully with proper run mount
# - NETALERTX_RUN shows as mounted and writable
# - No mount warnings since run directory can be non-persistent
# - Container starts normally with runtime files enabled
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-run_mounted
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_RUN: /services/run
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_system_services_run
target: /services/run
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,48 @@
# Expected outcome: Container shows mount error warning but continues running
# - NETALERTX_RUN shows as not mounted (❌ in Mount column)
# - Warning message displayed about configuration issues
# - Container continues to run despite the mount error
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-run_no-mount
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_RUN: /services/run
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,49 @@
# Expected outcome: Container shows dataloss risk warning for run on RAM disk
# - NETALERTX_RUN shows as mounted on tmpfs (RAM disk)
# - Dataloss risk warning since runtime files may be lost on restart
# - Container starts but runtime state may not persist
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-run_ramdisk
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_RUN: /services/run
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,52 @@
# Expected outcome: Container fails to start due to unwritable run partition
# - NETALERTX_RUN shows as mounted but unwritable (❌ in Writeable column)
# - 25-mandatory-folders.sh cannot create required runtime files and fails
# - Container startup fails because runtime infrastructure cannot be initialized
services:
netalertx:
network_mode: host
build:
context: ../../../
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-run_unwritable
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
environment:
LISTEN_ADDR: 0.0.0.0
PORT: 9999 # Use non-default port to test all paths
APP_CONF_OVERRIDE: 20212
ALWAYS_FRESH_INSTALL: true
NETALERTX_DEBUG: 0
SYSTEM_SERVICES_RUN: /services/run
volumes:
- type: volume
source: netalertx_db
target: /app/db
read_only: false
- type: volume
source: netalertx_config
target: /app/config
read_only: false
- type: volume
source: test_system_services_run
target: /services/run
read_only: true
tmpfs:
- "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:
test_netalertx_db:
test_netalertx_config:
test_netalertx_api:
test_netalertx_log:
test_system_services_run:
test_system_services_active_config:

View File

@@ -0,0 +1,61 @@
#!/bin/bash
# test_all_docker_composes.sh - Test all docker-compose configurations
# Extracts comments from each file and runs the container for 10 seconds
LOG_FILE="/workspaces/NetAlertX/test/docker_tests/configurations/test_results.log"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Starting Docker Compose Tests - $(date)" > "$LOG_FILE"
echo "==========================================" >> "$LOG_FILE"
# Function to extract comments from docker-compose file
extract_comments() {
local file="$1"
echo "File: $(basename "$file")" >> "$LOG_FILE"
echo "----------------------------------------" >> "$LOG_FILE"
# Extract lines starting with # until we hit a non-comment line
awk '
/^#/ {
# Remove the # and any leading/trailing whitespace
comment = substr($0, 2)
sub(/^ */, "", comment)
sub(/ *$/, "", comment)
if (comment != "") {
print comment
}
}
/^[^#]/ && !/^$/ {
exit
}
' "$file" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
}
# Function to run docker-compose test
run_test() {
local file="$1"
local dirname
dirname=$(dirname "$file")
local basename
basename=$(basename "$file")
echo "Testing: $basename" >> "$LOG_FILE"
echo "Directory: $dirname" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
echo "Running docker-compose up..." >> "$LOG_FILE"
timeout 10s docker-compose -f "$file" up 2>&1 >> "$LOG_FILE"
# Clean up
docker-compose -f "$file" down -v 2>/dev/null || true
docker volume prune -f 2>/dev/null || true
find "$SCRIPT_DIR" -name "docker-compose*.yml" -type f -print0 | sort -z | while IFS= read -r -d '' file; do
extract_comments "$file"
run_test "$file"
done
echo "All tests completed - $(date)" >> "$LOG_FILE"
echo "Results saved to: $LOG_FILE"

File diff suppressed because it is too large Load Diff

View File

@@ -197,6 +197,15 @@ def _run_container(
sleep_seconds: float = GRACE_SECONDS,
) -> subprocess.CompletedProcess[str]:
name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower()
# Clean up any existing container with this name
subprocess.run(
["docker", "rm", "-f", name],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
cmd: list[str] = ["docker", "run", "--rm", "--name", name]
if network_mode:
@@ -263,21 +272,23 @@ def _run_container(
)
result.output = stdouterr
# Print container output for debugging in every test run.
try:
print("\n--- CONTAINER out ---\n", result.output)
except Exception:
pass
print("\n--- CONTAINER OUTPUT START ---")
print(result.output)
print("--- CONTAINER OUTPUT END ---\n")
return result
def _assert_contains(result, snippet: str, cmd: list[str] = None) -> None:
if snippet not in result.output:
output = result.output + result.stderr
if snippet not in output:
cmd_str = " ".join(cmd) if cmd else ""
raise AssertionError(
f"Expected to find '{snippet}' in container output.\n"
f"Got:\n{result.output}\n"
f"STDOUT:\n{result.output}\n"
f"STDERR:\n{result.stderr}\n"
f"Combined output:\n{output}\n"
f"Container command:\n{cmd_str}"
)
@@ -313,485 +324,6 @@ def _restore_zero_perm_dir(paths: dict[str, pathlib.Path], key: str) -> None:
def test_root_owned_app_db_mount(tmp_path: pathlib.Path) -> None:
"""Test root-owned mounts - simulates mounting host directories owned by root.
1. Root-Owned Mounts: Simulates mounting host directories owned by root
(common with docker run -v /host/path:/app/db).
Tests each required mount point when owned by root user.
Expected: Warning about permission issues, guidance to fix ownership.
Check script: check-app-permissions.sh
Sample message: "⚠️ ATTENTION: Write permission denied. The application cannot write to..."
"""
paths = _setup_mount_tree(tmp_path, "root_app_db")
_chown_root(paths["app_db"])
volumes = _build_volume_args(paths)
try:
result = _run_container("root-app-db", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_db"]), result.args)
finally:
_chown_netalertx(paths["app_db"])
def test_root_owned_app_config_mount(tmp_path: pathlib.Path) -> None:
"""Test root-owned mounts - simulates mounting host directories owned by root.
1. Root-Owned Mounts: Simulates mounting host directories owned by root
(common with docker run -v /host/path:/app/db).
Tests each required mount point when owned by root user.
Expected: Warning about permission issues, guidance to fix ownership.
"""
paths = _setup_mount_tree(tmp_path, "root_app_config")
_chown_root(paths["app_config"])
volumes = _build_volume_args(paths)
try:
result = _run_container("root-app-config", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_config"]), result.args)
assert result.returncode != 0
finally:
_chown_netalertx(paths["app_config"])
def test_root_owned_app_log_mount(tmp_path: pathlib.Path) -> None:
"""Test root-owned mounts - simulates mounting host directories owned by root.
1. Root-Owned Mounts: Simulates mounting host directories owned by root
(common with docker run -v /host/path:/app/db).
Tests each required mount point when owned by root user.
Expected: Warning about permission issues, guidance to fix ownership.
"""
paths = _setup_mount_tree(tmp_path, "root_app_log")
_chown_root(paths["app_log"])
volumes = _build_volume_args(paths)
try:
result = _run_container("root-app-log", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_log"]), result.args)
assert result.returncode != 0
finally:
_chown_netalertx(paths["app_log"])
def test_root_owned_app_api_mount(tmp_path: pathlib.Path) -> None:
"""Test root-owned mounts - simulates mounting host directories owned by root.
1. Root-Owned Mounts: Simulates mounting host directories owned by root
(common with docker run -v /host/path:/app/db).
Tests each required mount point when owned by root user.
Expected: Warning about permission issues, guidance to fix ownership.
"""
paths = _setup_mount_tree(tmp_path, "root_app_api")
_chown_root(paths["app_api"])
volumes = _build_volume_args(paths)
try:
result = _run_container("root-app-api", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_api"]), result.args)
assert result.returncode != 0
finally:
_chown_netalertx(paths["app_api"])
def test_root_owned_nginx_conf_mount(tmp_path: pathlib.Path) -> None:
"""Test root-owned mounts - simulates mounting host directories owned by root.
1. Root-Owned Mounts: Simulates mounting host directories owned by root
(common with docker run -v /host/path:/app/db).
Tests each required mount point when owned by root user.
Expected: Warning about permission issues, guidance to fix ownership.
"""
paths = _setup_mount_tree(tmp_path, "root_nginx_conf")
_chown_root(paths["nginx_conf"])
volumes = _build_volume_args(paths)
try:
result = _run_container("root-nginx-conf", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["nginx_conf"]), result.args)
assert result.returncode != 0
finally:
_chown_netalertx(paths["nginx_conf"])
def test_root_owned_services_run_mount(tmp_path: pathlib.Path) -> None:
"""Test root-owned mounts - simulates mounting host directories owned by root.
1. Root-Owned Mounts: Simulates mounting host directories owned by root
(common with docker run -v /host/path:/app/db).
Tests each required mount point when owned by root user.
Expected: Warning about permission issues, guidance to fix ownership.
"""
paths = _setup_mount_tree(tmp_path, "root_services_run")
_chown_root(paths["services_run"])
volumes = _build_volume_args(paths)
try:
result = _run_container("root-services-run", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["services_run"]), result.args)
assert result.returncode != 0
finally:
_chown_netalertx(paths["services_run"])
def test_zero_permissions_app_db_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
Check script: check-app-permissions.sh
Sample messages: "⚠️ ATTENTION: Write permission denied. The application cannot write to..."
"⚠️ ATTENTION: Read permission denied. The application cannot read from..."
"""
paths = _setup_mount_tree(tmp_path, "chmod_app_db")
_setup_zero_perm_dir(paths, "app_db")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-db", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_db"]), result.args)
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "app_db")
def test_zero_permissions_app_db_file(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
"""
paths = _setup_mount_tree(tmp_path, "chmod_app_db_file")
(paths["app_db"] / "app.db").chmod(0)
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-db-file", volumes)
_assert_contains(result, "Write permission denied", result.args)
assert result.returncode != 0
finally:
(paths["app_db"] / "app.db").chmod(0o600)
def test_zero_permissions_app_config_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
"""
paths = _setup_mount_tree(tmp_path, "chmod_app_config")
_setup_zero_perm_dir(paths, "app_config")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-config", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_config"]), result.args)
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "app_config")
def test_zero_permissions_app_config_file(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
"""
paths = _setup_mount_tree(tmp_path, "chmod_app_config_file")
(paths["app_config"] / "app.conf").chmod(0)
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-config-file", volumes)
_assert_contains(result, "Write permission denied", result.args)
assert result.returncode != 0
finally:
(paths["app_config"] / "app.conf").chmod(0o600)
def test_zero_permissions_app_log_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
"""
paths = _setup_mount_tree(tmp_path, "chmod_app_log")
_setup_zero_perm_dir(paths, "app_log")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-log", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_log"]), result.args)
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "app_log")
def test_zero_permissions_app_api_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
"""
paths = _setup_mount_tree(tmp_path, "chmod_app_api")
_setup_zero_perm_dir(paths, "app_api")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-api", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_api"]), result.args)
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "app_api")
def test_zero_permissions_nginx_conf_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
"""
paths = _setup_mount_tree(tmp_path, "chmod_nginx_conf")
_setup_zero_perm_dir(paths, "nginx_conf")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-nginx-conf", volumes, user="20211:20211")
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "nginx_conf")
def test_zero_permissions_services_run_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: "Write permission denied" error with path, guidance to fix permissions.
"""
paths = _setup_mount_tree(tmp_path, "chmod_services_run")
_setup_zero_perm_dir(paths, "services_run")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-services-run", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["services_run"]), result.args)
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "services_run")
def test_readonly_app_db_mount(tmp_path: pathlib.Path) -> None:
"""Test readonly mounts - simulates read-only volume mounts in containers.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when mounted read-only.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "readonly_app_db")
volumes = _build_volume_args(paths, read_only={"app_db"})
result = _run_container("readonly-app-db", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_db"]), result.args)
assert result.returncode != 0
def test_readonly_app_config_mount(tmp_path: pathlib.Path) -> None:
"""Test readonly mounts - simulates read-only volume mounts in containers.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when mounted read-only.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "readonly_app_config")
volumes = _build_volume_args(paths, read_only={"app_config"})
result = _run_container("readonly-app-config", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_config"]), result.args)
assert result.returncode != 0
def test_readonly_app_log_mount(tmp_path: pathlib.Path) -> None:
"""Test readonly mounts - simulates read-only volume mounts in containers.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when mounted read-only.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "readonly_app_log")
volumes = _build_volume_args(paths, read_only={"app_log"})
result = _run_container("readonly-app-log", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_log"]), result.args)
assert result.returncode != 0
def test_readonly_app_api_mount(tmp_path: pathlib.Path) -> None:
"""Test readonly mounts - simulates read-only volume mounts in containers.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when mounted read-only.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "readonly_app_api")
volumes = _build_volume_args(paths, read_only={"app_api"})
result = _run_container("readonly-app-api", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["app_api"]), result.args)
assert result.returncode != 0
def test_readonly_nginx_conf_mount(tmp_path: pathlib.Path) -> None:
"""Test readonly mounts - simulates read-only volume mounts in containers.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when mounted read-only.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "readonly_nginx_conf")
volumes = _build_volume_args(paths, read_only={"nginx_conf"})
result = _run_container("readonly-nginx-conf", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, "/services/config/nginx/conf.active", result.args)
assert result.returncode != 0
def test_readonly_services_run_mount(tmp_path: pathlib.Path) -> None:
"""Test readonly mounts - simulates read-only volume mounts in containers.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when mounted read-only.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "readonly_services_run")
volumes = _build_volume_args(paths, read_only={"services_run"})
result = _run_container("readonly-services-run", volumes)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, str(VOLUME_MAP["services_run"]), result.args)
assert result.returncode != 0
def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None:
"""Test custom port configuration without writable nginx config mount.
4. Custom Port Without Nginx Config Mount: Simulates setting custom LISTEN_ADDR/PORT
without mounting nginx config. Container starts but uses default address.
Expected: Container starts but uses default address, warning about missing config mount.
Check script: check-nginx-config.sh
Sample messages: "⚠️ ATTENTION: Nginx configuration mount /services/config/nginx/conf.active is missing."
"⚠️ ATTENTION: Unable to write to /services/config/nginx/conf.active/netalertx.conf."
"""
paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf")
paths["nginx_conf"].chmod(0o500)
volumes = _build_volume_args(paths)
try:
result = _run_container(
"custom-port-ro-conf",
volumes,
env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"},
)
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, "/services/config/nginx/conf.active", result.args)
assert result.returncode != 0
finally:
paths["nginx_conf"].chmod(0o755)
def test_missing_mount_app_db(tmp_path: pathlib.Path) -> None:
"""Test missing required mounts - simulates forgetting to mount persistent volumes.
...
"""
paths = _setup_mount_tree(tmp_path, "missing_mount_app_db")
volumes = _build_volume_args(paths, skip={"app_db"})
# CHANGE: Run as root (0:0) to bypass all permission checks on other mounts.
result = _run_container("missing-mount-app-db", volumes, user="20211:20211")
# Acknowledge the original intent to check for permission denial (now implicit via root)
# _assert_contains(result, "Write permission denied", result.args) # No longer needed, as root user is used
# Robust assertion: check for both the warning and the path
if "not a persistent mount" not in result.output or "/app/db" not in result.output:
print("\n--- DEBUG CONTAINER OUTPUT ---\n", result.output)
raise AssertionError("Expected persistent mount warning for /app/db in container output.")
def test_missing_mount_app_config(tmp_path: pathlib.Path) -> None:
"""Test missing required mounts - simulates forgetting to mount persistent volumes.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when missing.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "missing_mount_app_config")
volumes = _build_volume_args(paths, skip={"app_config"})
result = _run_container("missing-mount-app-config", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, "/app/config", result.args)
def test_missing_mount_app_log(tmp_path: pathlib.Path) -> None:
"""Test missing required mounts - simulates forgetting to mount persistent volumes.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when missing.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "missing_mount_app_log")
volumes = _build_volume_args(paths, skip={"app_log"})
result = _run_container("missing-mount-app-log", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, "/app/log", result.args)
def test_missing_mount_app_api(tmp_path: pathlib.Path) -> None:
"""Test missing required mounts - simulates forgetting to mount persistent volumes.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when missing.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "missing_mount_app_api")
volumes = _build_volume_args(paths, skip={"app_api"})
result = _run_container("missing-mount-app-api", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, "/app/api", result.args)
def test_missing_mount_nginx_conf(tmp_path: pathlib.Path) -> None:
"""Test missing required mounts - simulates forgetting to mount persistent volumes.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when missing.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "missing_mount_nginx_conf")
volumes = _build_volume_args(paths, skip={"nginx_conf"})
result = _run_container("missing-mount-nginx-conf", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, "/services/config/nginx/conf.active", result.args)
assert result.returncode != 0
def test_missing_mount_services_run(tmp_path: pathlib.Path) -> None:
"""Test missing required mounts - simulates forgetting to mount persistent volumes.
3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes
in read-only containers. Tests each required mount point when missing.
Expected: "Write permission denied" error with path, guidance to add volume mounts.
"""
paths = _setup_mount_tree(tmp_path, "missing_mount_services_run")
volumes = _build_volume_args(paths, skip={"services_run"})
result = _run_container("missing-mount-services-run", volumes, user="20211:20211")
_assert_contains(result, "Write permission denied", result.args)
_assert_contains(result, "/services/run", result.args)
_assert_contains(result, "Container startup checks failed with exit code", result.args)
def test_missing_capabilities_triggers_warning(tmp_path: pathlib.Path) -> None:
"""Test missing required capabilities - simulates insufficient container privileges.
@@ -799,8 +331,8 @@ def test_missing_capabilities_triggers_warning(tmp_path: pathlib.Path) -> None:
NET_BIND_SERVICE capabilities. Required for ARP scanning and network operations.
Expected: "exec /bin/sh: operation not permitted" error, guidance to add capabilities.
Check script: check-cap.sh
Sample message: "⚠️ ATTENTION: Raw network capabilities are missing. Tools that rely on NET_RAW..."
Check script: N/A (capability check happens at container runtime)
Sample message: "exec /bin/sh: operation not permitted"
"""
paths = _setup_mount_tree(tmp_path, "missing_caps")
volumes = _build_volume_args(paths)
@@ -820,8 +352,8 @@ def test_running_as_root_is_blocked(tmp_path: pathlib.Path) -> None:
dedicated netalertx user. Warning about security risks, special permission fix mode.
Expected: Warning about security risks, guidance to use UID 20211.
Check script: check-app-permissions.sh
Sample message: "⚠️ ATTENTION: NetAlertX is running as root (UID 0). This defeats every hardening..."
Check script: /entrypoint.d/0-storage-permission.sh
Sample message: "🚨 CRITICAL SECURITY ALERT: NetAlertX is running as ROOT (UID 0)!"
"""
paths = _setup_mount_tree(tmp_path, "run_as_root")
volumes = _build_volume_args(paths)
@@ -832,7 +364,7 @@ def test_running_as_root_is_blocked(tmp_path: pathlib.Path) -> None:
)
_assert_contains(result, "NetAlertX is running as ROOT", result.args)
_assert_contains(result, "Permissions fixed for read-write paths.", result.args)
assert result.returncode == 0 # container must be forced to exit 0 by termination after warning
assert result.returncode == 0 # container warns but continues running, then terminated by test framework
def test_running_as_uid_1000_warns(tmp_path: pathlib.Path) -> None:
@@ -843,7 +375,7 @@ def test_running_as_uid_1000_warns(tmp_path: pathlib.Path) -> None:
of netalertx user. Permission errors due to incorrect user context.
Expected: Permission errors, guidance to use correct user.
Check script: check-user-netalertx.sh
Check script: /entrypoint.d/60-user-netalertx.sh
Sample message: "⚠️ ATTENTION: NetAlertX is running as UID 1000:1000. Hardened permissions..."
"""
paths = _setup_mount_tree(tmp_path, "run_as_1000")
@@ -854,7 +386,7 @@ def test_running_as_uid_1000_warns(tmp_path: pathlib.Path) -> None:
user="1000:1000",
)
_assert_contains(result, "NetAlertX is running as UID 1000:1000", result.args)
assert result.returncode != 0
def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
@@ -868,7 +400,17 @@ def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
Check script: check-network-mode.sh
Sample message: "⚠️ ATTENTION: NetAlertX is not running with --network=host. Bridge networking..."
"""
paths = _setup_mount_tree(tmp_path, "missing_host_net")
base = tmp_path / "missing_host_net_base"
paths = _setup_fixed_mount_tree(base)
# Ensure directories are writable and owned by netalertx user so container can operate
for key in ["app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"]:
paths[key].chmod(0o777)
_chown_netalertx(paths[key])
# Create a config file so the writable check passes
config_file = paths["app_config"] / "app.conf"
config_file.write_text("test config")
config_file.chmod(0o666)
_chown_netalertx(config_file)
volumes = _build_volume_args(paths)
result = _run_container(
"missing-host-network",
@@ -876,7 +418,6 @@ def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
network_mode=None,
)
_assert_contains(result, "not running with --network=host", result.args)
assert result.returncode != 0
def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None:
@@ -885,15 +426,21 @@ def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None:
9. Missing Configuration File: Simulates corrupted/missing app.conf.
Container automatically regenerates default configuration on startup.
Expected: Automatic regeneration of default configuration.
Check script: /entrypoint.d/15-first-run-config.sh
Sample message: "Default configuration written to"
"""
base = tmp_path / "missing_app_conf_base"
paths = _setup_fixed_mount_tree(base)
_chown_netalertx(paths["app_config"])
# Ensure directories are writable and owned by netalertx user so container can operate
for key in ["app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"]:
paths[key].chmod(0o777)
_chown_netalertx(paths[key])
(paths["app_config"] / "testfile.txt").write_text("test")
volumes = _build_volume_args(paths)
result = _run_container("missing-app-conf", volumes)
result = _run_container("missing-app-conf", volumes, sleep_seconds=5)
_assert_contains(result, "Default configuration written to", result.args)
assert result.returncode != 0
assert result.returncode == 0
def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None:
@@ -902,54 +449,253 @@ def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None:
10. Missing Database File: Simulates corrupted/missing app.db.
Container automatically creates initial database schema on startup.
Expected: Automatic creation of initial database schema.
Check script: /entrypoint.d/20-first-run-db.sh
Sample message: "Building initial database schema"
"""
base = tmp_path / "missing_app_db_base"
paths = _setup_fixed_mount_tree(base)
_chown_netalertx(paths["app_db"])
(paths["app_db"] / "testfile.txt").write_text("test")
volumes = _build_volume_args(paths)
result = _run_container("missing-app-db", volumes, user="20211:20211")
result = _run_container("missing-app-db", volumes, user="20211:20211", sleep_seconds=5)
_assert_contains(result, "Building initial database schema", result.args)
assert result.returncode != 0
def test_tmpfs_config_mount_warns(tmp_path: pathlib.Path) -> None:
"""Test tmpfs instead of volumes - simulates using tmpfs for persistent data.
def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None:
"""Test custom port configuration without writable nginx config mount.
11. Tmpfs Instead of Volumes: Simulates using tmpfs mounts instead of persistent volumes
(data loss on restart). Tests config and db directories mounted as tmpfs.
Expected: "Read permission denied" error, guidance to use persistent volumes.
4. Custom Port Without Nginx Config Mount: Simulates setting custom LISTEN_ADDR/PORT
without mounting nginx config. Container starts but uses default address.
Expected: Container starts but uses default address, warning about missing config mount.
Check scripts: check-storage.sh, check-storage-extra.sh
Sample message: "⚠️ ATTENTION: /app/config is not a persistent mount. Your data in this directory..."
Check script: check-nginx-config.sh
Sample messages: "⚠️ ATTENTION: Nginx configuration mount /services/config/nginx/conf.active is missing."
"⚠️ ATTENTION: Unable to write to /services/config/nginx/conf.active/netalertx.conf."
TODO: Custom ports can only be assigned when we have the PORT=something, and in that case
the /config.active partition shows up in the messages. It SHOULD exit if port is specified
and not writeable and I'm not sure it will.
RESOLVED: When PORT is specified but nginx config is not writable, the container warns
"Unable to write to /services/config/nginx/conf.active/netalertx.conf" but does NOT exit.
It continues with startup and fails later for other reasons if any directories are not writable.
"""
paths = _setup_mount_tree(tmp_path, "tmpfs_config")
volumes = _build_volume_args(paths, skip={"app_config"})
extra = ["--mount", "type=tmpfs,destination=/app/config"]
result = _run_container(
"tmpfs-config",
volumes,
extra_args=extra,
)
_assert_contains(result, "not a persistent mount.", result.args)
_assert_contains(result, "/app/config", result.args)
paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf")
# Ensure other directories are writable so container gets to nginx config check
for key in ["app_db", "app_config", "app_log", "app_api", "services_run"]:
paths[key].chmod(0o777)
paths["nginx_conf"].chmod(0o500)
volumes = _build_volume_args(paths)
try:
result = _run_container(
"custom-port-ro-conf",
volumes,
env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"},
user="20211:20211",
sleep_seconds=5,
)
_assert_contains(result, "Unable to write to", result.args)
_assert_contains(result, "/services/config/nginx/conf.active/netalertx.conf", result.args)
# TODO: Should this exit when PORT is specified but nginx config is not writable?
# Currently it just warns and continues
assert result.returncode != 0
finally:
paths["nginx_conf"].chmod(0o755)
def test_zero_permissions_app_db_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
def test_tmpfs_db_mount_warns(tmp_path: pathlib.Path) -> None:
"""Test tmpfs instead of volumes - simulates using tmpfs for persistent data.
11. Tmpfs Instead of Volumes: Simulates using tmpfs mounts instead of persistent volumes
(data loss on restart). Tests config and db directories mounted as tmpfs.
Expected: "Read permission denied" error, guidance to use persistent volumes.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: Mounts table shows ❌ for writeable status, configuration issues detected.
"""
paths = _setup_mount_tree(tmp_path, "tmpfs_db")
volumes = _build_volume_args(paths, skip={"app_db"})
extra = ["--mount", "type=tmpfs,destination=/app/db"]
paths = _setup_mount_tree(tmp_path, "chmod_app_db")
_setup_zero_perm_dir(paths, "app_db")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-db", volumes, user="20211:20211")
# Check that the mounts table shows the app_db directory as not writeable
_assert_contains(result, "/app/db | ❌ |", result.args)
# Check that configuration issues are detected
_assert_contains(result, "Configuration issues detected", result.args)
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "app_db")
def test_zero_permissions_app_config_dir(tmp_path: pathlib.Path) -> None:
"""Test zero permissions - simulates mounting directories/files with no permissions.
2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000).
Tests directories and files with no read/write/execute permissions.
Expected: Mounts table shows ❌ for writeable status, configuration issues detected.
"""
paths = _setup_mount_tree(tmp_path, "chmod_app_config")
_setup_zero_perm_dir(paths, "app_config")
volumes = _build_volume_args(paths)
try:
result = _run_container("chmod-app-config", volumes, user="20211:20211")
# Check that the mounts table shows the app_config directory as not writeable
_assert_contains(result, "/app/config | ❌ |", result.args)
# Check that configuration issues are detected
_assert_contains(result, "Configuration issues detected", result.args)
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "app_config")
def test_mandatory_folders_creation(tmp_path: pathlib.Path) -> None:
"""Test mandatory folders creation - simulates missing plugins log directory.
1. Mandatory Folders: Simulates missing required directories and log files.
Container automatically creates plugins log, system services run log/tmp directories,
and required log files on startup.
Expected: Automatic creation of all required directories and files.
Check script: 25-mandatory-folders.sh
Sample message: "Creating Plugins log"
"""
paths = _setup_mount_tree(tmp_path, "mandatory_folders")
# Remove the plugins log directory to simulate missing mandatory folder
plugins_log_dir = paths["app_log"] / "plugins"
if plugins_log_dir.exists():
shutil.rmtree(plugins_log_dir)
# Ensure other directories are writable and owned by netalertx user so container gets past mounts.py
for key in ["app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"]:
paths[key].chmod(0o777)
_chown_netalertx(paths[key]) # Ensure all directories are owned by netalertx
volumes = _build_volume_args(paths)
result = _run_container("mandatory-folders", volumes, user="20211:20211", sleep_seconds=5)
_assert_contains(result, "Creating Plugins log", result.args)
# The container will fail at writable config due to permission issues, but we just want to verify
# that mandatory folders creation ran successfully
def test_writable_config_validation(tmp_path: pathlib.Path) -> None:
"""Test writable config validation - simulates read-only config file.
3. Writable Config Validation: Simulates config file with read-only permissions.
Container verifies it can read from and write to critical config and database files.
Expected: "Read permission denied" warning for config file.
Check script: 30-writable-config.sh
Sample message: "Read permission denied"
"""
paths = _setup_mount_tree(tmp_path, "writable_config")
# Make config file read-only but keep directories writable so container gets past mounts.py
config_file = paths["app_config"] / "app.conf"
config_file.chmod(0o400) # Read-only for owner
# Ensure directories are writable and owned by netalertx user so container gets past mounts.py
for key in ["app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"]:
paths[key].chmod(0o777)
_chown_netalertx(paths[key])
volumes = _build_volume_args(paths)
result = _run_container("writable-config", volumes, user="20211:20211", sleep_seconds=5.0)
_assert_contains(result, "Read permission denied", result.args)
def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None:
"""Test excessive capabilities detection - simulates container with extra capabilities.
11. Excessive Capabilities: Simulates container with capabilities beyond the required
NET_ADMIN, NET_RAW, and NET_BIND_SERVICE.
Expected: Warning about excessive capabilities detected.
Check script: 90-excessive-capabilities.sh
Sample message: "Excessive capabilities detected"
"""
paths = _setup_mount_tree(tmp_path, "excessive_caps")
volumes = _build_volume_args(paths)
# Add excessive capabilities beyond the required ones
result = _run_container(
"tmpfs-db",
"excessive-caps",
volumes,
extra_args=extra,
extra_args=["--cap-add=SYS_ADMIN", "--cap-add=NET_BROADCAST"],
sleep_seconds=5,
)
_assert_contains(result, "not a persistent mount.", result.args)
_assert_contains(result, "/app/db", result.args)
_assert_contains(result, "Excessive capabilities detected", result.args)
_assert_contains(result, "bounding caps:", result.args)
# This warning doesn't cause failure by itself, but other issues might
def test_appliance_integrity_read_write_mode(tmp_path: pathlib.Path) -> None:
"""Test appliance integrity - simulates running with read-write root filesystem.
12. Appliance Integrity: Simulates running container with read-write root filesystem
instead of read-only mode.
Expected: Warning about running in read-write mode instead of read-only.
Check script: 95-appliance-integrity.sh
Sample message: "Container is running as read-write, not in read-only mode"
"""
paths = _setup_mount_tree(tmp_path, "appliance_integrity")
volumes = _build_volume_args(paths)
# Container runs read-write by default (not mounting root as read-only)
result = _run_container("appliance-integrity", volumes, sleep_seconds=5)
_assert_contains(result, "Container is running as read-write, not in read-only mode", result.args)
_assert_contains(result, "read-only: true", result.args)
# This warning doesn't cause failure by itself, but other issues might
def test_mount_analysis_ram_disk_performance(tmp_path: pathlib.Path) -> None:
"""Test mount analysis for RAM disk performance issues.
Tests 10-mounts.py detection of persistent paths on RAM disks (tmpfs) which can cause
performance issues and data loss on container restart.
Expected: Mounts table shows ❌ for RAMDisk on persistent paths, performance warnings.
Check script: 10-mounts.py
Sample message: "Configuration issues detected"
"""
paths = _setup_mount_tree(tmp_path, "ram_disk_mount")
# Mount persistent paths (db, config) on tmpfs to simulate RAM disk
volumes = [
(str(paths["app_log"]), "/app/log", False),
(str(paths["app_api"]), "/app/api", False),
(str(paths["services_run"]), "/services/run", False),
(str(paths["nginx_conf"]), "/services/config/nginx/conf.active", False),
]
# Use tmpfs mounts for persistent paths with proper permissions
extra_args = ["--tmpfs", "/app/db:uid=20211,gid=20211,mode=755", "--tmpfs", "/app/config:uid=20211,gid=20211,mode=755"]
result = _run_container("ram-disk-mount", volumes=volumes, extra_args=extra_args, user="20211:20211")
# Check that mounts table shows RAM disk detection for persistent paths
_assert_contains(result, "/app/db | ✅ | ✅ | ❌ | | ❌", result.args)
_assert_contains(result, "/app/config | ✅ | ✅ | ❌ | | ❌", result.args)
# Check that configuration issues are detected due to dataloss risk
_assert_contains(result, "Configuration issues detected", result.args)
assert result.returncode != 0
def test_mount_analysis_dataloss_risk(tmp_path: pathlib.Path) -> None:
"""Test mount analysis for dataloss risk on non-persistent filesystems.
Tests 10-mounts.py detection when persistent database/config paths are
mounted on non-persistent filesystems (tmpfs, ramfs).
Expected: Mounts table shows dataloss risk warnings for persistent paths on tmpfs.
Check script: 10-mounts.py
Sample message: "Configuration issues detected"
"""
paths = _setup_mount_tree(tmp_path, "dataloss_risk")
# Mount persistent paths (db, config) on tmpfs to simulate non-persistent storage
volumes = [
(str(paths["app_log"]), "/app/log", False),
(str(paths["app_api"]), "/app/api", False),
(str(paths["services_run"]), "/services/run", False),
(str(paths["nginx_conf"]), "/services/config/nginx/conf.active", False),
]
# Use tmpfs mounts for persistent paths with proper permissions
extra_args = ["--tmpfs", "/app/db:uid=20211,gid=20211,mode=755", "--tmpfs", "/app/config:uid=20211,gid=20211,mode=755"]
result = _run_container("dataloss-risk", volumes=volumes, extra_args=extra_args, user="20211:20211")
# Check that mounts table shows dataloss risk for persistent paths on tmpfs
_assert_contains(result, "/app/db | ✅ | ✅ | ❌ | | ❌", result.args)
_assert_contains(result, "/app/config | ✅ | ✅ | ❌ | | ❌", result.args)
# Check that configuration issues are detected due to dataloss risk
_assert_contains(result, "Configuration issues detected", result.args)
assert result.returncode != 0

View File

@@ -0,0 +1,447 @@
'''
Docker Compose integration tests for NetAlertX startup scenarios.
This set of tests requires netalertx-test image built and docker compose.
Ensure netalertx-test image is built prior to starting these tests.
'''
import copy
import os
import pathlib
import re
import subprocess
import pytest
import yaml
# Path to test configurations
CONFIG_DIR = pathlib.Path(__file__).parent / "configurations"
ANSI_ESCAPE = re.compile(r"\x1B\[[0-9;]*[A-Za-z]")
pytestmark = [pytest.mark.docker, pytest.mark.compose]
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
# Docker Compose configurations for different test scenarios
COMPOSE_CONFIGS = {
"missing_capabilities": {
"services": {
"netalertx": {
"image": IMAGE,
"network_mode": "host",
"userns_mode": "host",
"cap_drop": ["ALL"], # Drop all capabilities
"tmpfs": ["/tmp:mode=777"],
"volumes": [
"./test_data/app_db:/app/db",
"./test_data/app_config:/app/config",
"./test_data/app_log:/app/log",
"./test_data/app_api:/app/api",
"./test_data/nginx_conf:/services/config/nginx/conf.active",
"./test_data/services_run:/services/run"
],
"environment": {
"TZ": "UTC"
}
}
}
},
"host_network": {
"services": {
"netalertx": {
"image": IMAGE,
"network_mode": "host",
"userns_mode": "host",
"cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"],
"tmpfs": ["/tmp:mode=777"],
"volumes": [
"./test_data/app_db:/app/db",
"./test_data/app_config:/app/config",
"./test_data/app_log:/app/log",
"./test_data/app_api:/app/api",
"./test_data/nginx_conf:/services/config/nginx/conf.active",
"./test_data/services_run:/services/run"
],
"environment": {
"TZ": "UTC"
}
}
}
},
"normal_startup": {
"services": {
"netalertx": {
"image": IMAGE,
"network_mode": "host",
"userns_mode": "host",
"read_only": True,
"cap_drop": ["ALL"],
"cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"],
"user": "20211:20211",
"tmpfs": [
"/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime",
"/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,sync,noatime,nodiratime",
"/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime",
"/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime",
"/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime",
],
"volumes": [
{
"type": "volume",
"source": "__CONFIG_VOLUME__",
"target": "/app/config",
"read_only": False,
},
{
"type": "volume",
"source": "__DB_VOLUME__",
"target": "/app/db",
"read_only": False,
},
{
"type": "bind",
"source": "/etc/localtime",
"target": "/etc/localtime",
"read_only": True,
},
],
"environment": {
"TZ": "UTC"
}
}
}
}
}
def _create_test_data_dirs(base_dir: pathlib.Path) -> None:
"""Create test data directories and files with write permissions for the container user."""
dirs = ["app_db", "app_config", "app_log", "app_api", "nginx_conf", "services_run"]
for dir_name in dirs:
dir_path = base_dir / "test_data" / dir_name
dir_path.mkdir(parents=True, exist_ok=True)
dir_path.chmod(0o777)
# Create basic config file
config_file = base_dir / "test_data" / "app_config" / "app.conf"
if not config_file.exists():
config_file.write_text("# Test configuration\n")
config_file.chmod(0o666)
# Create basic db file
db_file = base_dir / "test_data" / "app_db" / "app.db"
if not db_file.exists():
# Create a minimal SQLite database
import sqlite3
conn = sqlite3.connect(str(db_file))
conn.close()
db_file.chmod(0o666)
def _run_docker_compose(
compose_file: pathlib.Path,
project_name: str,
timeout: int = 5,
env_vars: dict | None = None,
detached: bool = False,
) -> subprocess.CompletedProcess:
"""Run docker compose up and capture output."""
cmd = [
"docker", "compose",
"-f", str(compose_file),
"-p", project_name,
]
up_cmd = cmd + ["up"]
if detached:
up_cmd.append("-d")
else:
up_cmd.extend([
"--abort-on-container-exit",
"--timeout", str(timeout)
])
# Merge custom env vars with current environment
env = os.environ.copy()
if env_vars:
env.update(env_vars)
try:
if detached:
up_result = subprocess.run(
up_cmd,
cwd=compose_file.parent,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout,
check=False,
env=env,
)
logs_cmd = cmd + ["logs"]
logs_result = subprocess.run(
logs_cmd,
cwd=compose_file.parent,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout,
check=False,
env=env,
)
result = subprocess.CompletedProcess(
up_cmd,
up_result.returncode,
stdout=(up_result.stdout or "") + (logs_result.stdout or ""),
stderr=(up_result.stderr or "") + (logs_result.stderr or ""),
)
else:
result = subprocess.run(
up_cmd,
cwd=compose_file.parent,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout + 10,
check=False,
env=env,
)
except subprocess.TimeoutExpired:
# Clean up on timeout
subprocess.run(["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v"],
cwd=compose_file.parent, check=False, env=env)
raise
# Always clean up
subprocess.run(["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v"],
cwd=compose_file.parent, check=False, env=env)
# Combine stdout and stderr
result.output = result.stdout + result.stderr
# Surface command context and IO for any caller to aid debugging
print("\n[compose command]", " ".join(up_cmd))
print("[compose cwd]", str(compose_file.parent))
print("[compose stdin]", "<none>")
if result.stdout:
print("[compose stdout]\n" + result.stdout)
if result.stderr:
print("[compose stderr]\n" + result.stderr)
if detached:
logs_cmd_display = cmd + ["logs"]
print("[compose logs command]", " ".join(logs_cmd_display))
return result
def test_missing_capabilities_compose() -> None:
"""Test missing required capabilities using docker compose.
Uses docker-compose.missing-caps.yml which drops all capabilities.
Expected: "exec /bin/sh: operation not permitted" error.
"""
compose_file = CONFIG_DIR / "docker-compose.missing-caps.yml"
result = _run_docker_compose(compose_file, "netalertx-missing-caps")
# Check for expected error
assert "exec /bin/sh: operation not permitted" in result.output
assert result.returncode != 0
def test_custom_port_with_unwritable_nginx_config_compose() -> None:
"""Test custom port configuration with unwritable nginx config using docker compose.
Uses docker-compose.mount-test.active_config_unwritable.yml with PORT=24444.
Expected: Container shows warning about unable to write nginx config.
"""
compose_file = CONFIG_DIR / "mount-tests" / "docker-compose.mount-test.active_config_unwritable.yml"
result = _run_docker_compose(compose_file, "netalertx-custom-port", env_vars={"PORT": "24444"})
# Check for nginx config write failure warning
assert "Unable to write to /services/config/nginx/conf.active/netalertx.conf" in result.output
# Container should still attempt to start but may fail for other reasons
# The key is that the nginx config write warning appears
def test_host_network_compose(tmp_path: pathlib.Path) -> None:
"""Test host networking mode using docker compose.
Simulates running with network_mode: host.
Expected: Container starts successfully with host networking.
"""
base_dir = tmp_path / "host_network"
base_dir.mkdir()
# Create test data directories
_create_test_data_dirs(base_dir)
# Create compose file
compose_config = COMPOSE_CONFIGS["host_network"].copy()
compose_file = base_dir / "docker-compose.yml"
with open(compose_file, 'w') as f:
yaml.dump(compose_config, f)
# Run docker compose
result = _run_docker_compose(compose_file, "netalertx-host-net")
# Check that it doesn't fail with network-related errors
assert "not running with --network=host" not in result.output
# Container should start (may fail later for other reasons, but network should be OK)
def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
"""Test normal startup with expected warnings using docker compose.
Simulates proper configuration with all required settings.
Expected: Container starts and shows expected warnings with pipe characters (═).
This demonstrates what a "normal" startup looks like with warnings.
"""
base_dir = tmp_path / "normal_startup"
base_dir.mkdir()
project_name = "netalertx-normal"
# Create compose file mirroring production docker-compose.yml
compose_config = copy.deepcopy(COMPOSE_CONFIGS["normal_startup"])
service = compose_config["services"]["netalertx"]
config_volume_name = f"{project_name}_config"
db_volume_name = f"{project_name}_db"
service["volumes"][0]["source"] = config_volume_name
service["volumes"][1]["source"] = db_volume_name
service.setdefault("environment", {})
service["environment"].update({
"PORT": "22111",
"GRAPHQL_PORT": "22112",
})
compose_config["volumes"] = {
config_volume_name: {},
db_volume_name: {},
}
compose_file = base_dir / "docker-compose.yml"
with open(compose_file, 'w') as f:
yaml.dump(compose_config, f)
# Run docker compose
result = _run_docker_compose(compose_file, project_name, detached=True)
clean_output = ANSI_ESCAPE.sub("", result.output)
# Check that startup completed without critical issues and mounts table shows success
assert "Startup pre-checks" in clean_output
assert "" not in clean_output
assert "/app/db | ✅" in clean_output
# Ensure no critical errors or permission problems surfaced
assert "Write permission denied" not in clean_output
assert "CRITICAL" not in clean_output
assert "⚠️" not in clean_output
def test_ram_disk_mount_analysis_compose(tmp_path: pathlib.Path) -> None:
"""Test mount analysis for RAM disk detection using docker compose.
Simulates mounting persistent paths on tmpfs (RAM disk).
Expected: Mounts table shows ❌ for RAMDisk on persistent paths, dataloss warnings.
"""
base_dir = tmp_path / "ram_disk_test"
base_dir.mkdir()
# Create test data directories
_create_test_data_dirs(base_dir)
# Create compose file with tmpfs mounts for persistent paths
compose_config = {
"services": {
"netalertx": {
"image": IMAGE,
"network_mode": "host",
"userns_mode": "host",
"cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"],
"user": "20211:20211",
"tmpfs": [
"/tmp:mode=777",
"/app/db", # RAM disk for persistent DB
"/app/config" # RAM disk for persistent config
],
"volumes": [
f"./test_data/app_log:/app/log",
f"./test_data/app_api:/app/api",
f"./test_data/nginx_conf:/services/config/nginx/conf.active",
f"./test_data/services_run:/services/run"
],
"environment": {
"TZ": "UTC"
}
}
}
}
compose_file = base_dir / "docker-compose.yml"
with open(compose_file, 'w') as f:
yaml.dump(compose_config, f)
# Run docker compose
result = _run_docker_compose(compose_file, "netalertx-ram-disk")
# Check that mounts table shows RAM disk detection and dataloss warnings
assert "Configuration issues detected" in result.output
assert "/app/db" in result.output
assert "/app/config" in result.output
assert result.returncode != 0 # Should fail due to dataloss risk
def test_dataloss_risk_mount_analysis_compose(tmp_path: pathlib.Path) -> None:
"""Test mount analysis for dataloss risk using docker compose.
Simulates mounting persistent paths on non-persistent tmpfs.
Expected: Mounts table shows dataloss risk warnings for persistent paths.
"""
base_dir = tmp_path / "dataloss_test"
base_dir.mkdir()
# Create test data directories
_create_test_data_dirs(base_dir)
# Create compose file with tmpfs for persistent data
compose_config = {
"services": {
"netalertx": {
"image": IMAGE,
"network_mode": "host",
"userns_mode": "host",
"cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"],
"user": "20211:20211",
"tmpfs": [
"/tmp:mode=777",
"/app/db:uid=20211,gid=20211", # Non-persistent for DB
"/app/config:uid=20211,gid=20211" # Non-persistent for config
],
"volumes": [
f"./test_data/app_log:/app/log",
f"./test_data/app_api:/app/api",
f"./test_data/nginx_conf:/services/config/nginx/conf.active",
f"./test_data/services_run:/services/run"
],
"environment": {
"TZ": "UTC"
}
}
}
}
compose_file = base_dir / "docker-compose.yml"
with open(compose_file, 'w') as f:
yaml.dump(compose_config, f)
# Run docker compose
result = _run_docker_compose(compose_file, "netalertx-dataloss")
# Check that mounts table shows dataloss risk detection
assert "Configuration issues detected" in result.output
assert "/app/db" in result.output
assert "/app/config" in result.output
assert result.returncode != 0 # Should fail due to dataloss risk

View File

@@ -0,0 +1,433 @@
#!/usr/bin/env python3
"""
Pytest-based Mount Diagnostic Tests for NetAlertX
Tests all possible mount configurations for each path to validate the diagnostic tool.
Uses pytest framework for proper test discovery and execution.
All tests use the mounts table. For reference, the mounts table looks like this:
Path | Writeable | Mount | RAMDisk | Performance | DataLoss
------------------------------------+-----------+-------+---------+-------------+----------
/app/db | ✅ | ❌ | | | ❌
/app/config | ✅ | ❌ | | | ❌
/app/api | ✅ | ❌ | ❌ | ❌ | ✅
/app/log | ✅ | ❌ | ❌ | ❌ | ✅
/services/run | ✅ | ❌ | ❌ | ❌ | ✅
/services/config/nginx/conf.active | ✅ | ❌ | ❌ | ❌ | ✅
Table Assertions:
- Use assert_table_row(output, path, writeable=True/False/None, mount=True/False/None, ...)
- Emojis are converted: ✅=True, ❌=False, =None
- Example: assert_table_row(output, "/app/db", writeable=True, mount=False, dataloss=False)
"""
import os
import subprocess
import pytest
import re
from pathlib import Path
from dataclasses import dataclass
from typing import List, Optional, Tuple, Union
# Test configurations directory
CONFIG_DIR = Path(__file__).parent / "configurations"
@dataclass
class MountTableRow:
"""Represents a parsed row from the mount diagnostic table."""
path: str
writeable: bool
mount: bool
ramdisk: Optional[bool] # None for
performance: Optional[bool] # None for
dataloss: bool
def parse_mount_table(output: str) -> List[MountTableRow]:
"""Parse the mount diagnostic table from stdout."""
rows = []
# Find the table in the output
lines = output.split('\n')
table_start = None
for i, line in enumerate(lines):
if line.startswith(' Path ') and '|' in line:
table_start = i
break
if table_start is None:
return rows
# Skip header and separator lines
data_lines = lines[table_start + 2:]
for line in data_lines:
if '|' not in line or line.strip() == '':
continue
# Split by | and clean up
parts = [part.strip() for part in line.split('|')]
if len(parts) < 6:
continue
path = parts[0]
if not path:
continue
# Convert emojis to boolean/none
def emoji_to_bool(emoji: str) -> Optional[bool]:
emoji = emoji.strip()
if emoji == '':
return True
elif emoji == '':
return False
elif emoji == '':
return None
return None
try:
row = MountTableRow(
path=path,
writeable=emoji_to_bool(parts[1]),
mount=emoji_to_bool(parts[2]),
ramdisk=emoji_to_bool(parts[3]),
performance=emoji_to_bool(parts[4]),
dataloss=emoji_to_bool(parts[5])
)
rows.append(row)
except (IndexError, ValueError):
continue
return rows
def assert_table_row(output: str, expected_path: str,
writeable: Optional[bool] = None,
mount: Optional[bool] = None,
ramdisk: Optional[bool] = None,
performance: Optional[bool] = None,
dataloss: Optional[bool] = None) -> MountTableRow:
"""Assert that a specific table row matches expected values."""
rows = parse_mount_table(output)
# Find the row for the expected path
matching_row = None
for row in rows:
if row.path == expected_path:
matching_row = row
break
assert matching_row is not None, f"Path '{expected_path}' not found in table. Available paths: {[r.path for r in rows]}"
# Check each field if specified
if writeable is not None:
assert matching_row.writeable == writeable, f"Path '{expected_path}': expected writeable={writeable}, got {matching_row.writeable}"
if mount is not None:
assert matching_row.mount == mount, f"Path '{expected_path}': expected mount={mount}, got {matching_row.mount}"
if ramdisk is not None:
assert matching_row.ramdisk == ramdisk, f"Path '{expected_path}': expected ramdisk={ramdisk}, got {matching_row.ramdisk}"
if performance is not None:
assert matching_row.performance == performance, f"Path '{expected_path}': expected performance={performance}, got {matching_row.performance}"
if dataloss is not None:
assert matching_row.dataloss == dataloss, f"Path '{expected_path}': expected dataloss={dataloss}, got {matching_row.dataloss}"
return matching_row
@dataclass
class TestScenario:
"""Represents a test scenario for a specific path configuration."""
__test__ = False # Prevent pytest from collecting this as a test class
name: str
path_var: str
container_path: str
is_persistent: bool
docker_compose: str
expected_issues: List[str] # List of expected issue types
expected_exit_code: int # Expected container exit code
@pytest.fixture(scope="session")
def netalertx_test_image():
"""Ensure the netalertx-test image exists."""
image_name = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
# Check if image exists
result = subprocess.run(
["docker", "images", "-q", image_name],
capture_output=True,
text=True
)
if not result.stdout.strip():
pytest.skip(f"NetAlertX test image '{image_name}' not found. Build it first.")
return image_name
@pytest.fixture
def test_scenario(request):
"""Fixture that provides test scenarios."""
return request.param
def create_test_scenarios() -> List[TestScenario]:
"""Create all test scenarios."""
scenarios = []
# Define paths to test
paths = [
("db", "/app/db", True, "NETALERTX_DB"),
("config", "/app/config", True, "NETALERTX_CONFIG"),
("api", "/app/api", False, "NETALERTX_API"),
("log", "/app/log", False, "NETALERTX_LOG"),
("run", "/services/run", False, "SYSTEM_SERVICES_RUN"),
("active_config", "/services/config/nginx/conf.active", False, "SYSTEM_SERVICES_ACTIVE_CONFIG"),
]
# Test scenarios for each path
test_scenarios = [
("no-mount", ["table_issues", "warning_message"]), # Always issues
("ramdisk", []), # Good for non-persistent, bad for persistent
("mounted", ["table_issues", "warning_message"]), # Bad for non-persistent, good for persistent
("unwritable", ["table_issues", "warning_message"]), # Always issues
]
for path_name, container_path, is_persistent, env_var in paths:
for scenario_name, base_expected_issues in test_scenarios:
# Adjust expected issues based on persistence and scenario
expected_issues = list(base_expected_issues) # Copy the list
if scenario_name == "ramdisk" and is_persistent:
# Ramdisk is bad for persistent paths
expected_issues = ["table_issues", "warning_message"]
elif scenario_name == "mounted" and is_persistent:
# Mounted is good for persistent paths
expected_issues = []
elif path_name == "active_config" and scenario_name == "unwritable":
# active_config unwritable: RAM disk issues detected
expected_issues = ["table_issues", "warning_message"]
compose_file = f"docker-compose.mount-test.{path_name}_{scenario_name}.yml"
# Determine expected exit code
expected_exit_code = 1 if scenario_name == "unwritable" else 0
scenarios.append(TestScenario(
name=f"{path_name}_{scenario_name}",
path_var=env_var,
container_path=container_path,
is_persistent=is_persistent,
docker_compose=compose_file,
expected_issues=expected_issues,
expected_exit_code=expected_exit_code
))
return scenarios
def validate_scenario_table_output(output: str, test_scenario: TestScenario) -> None:
"""Validate the diagnostic table for scenarios that should report issues."""
if not test_scenario.expected_issues:
return
try:
if test_scenario.name.startswith('db_'):
if test_scenario.name == 'db_ramdisk':
# db on ramdisk: mount=True, ramdisk=False (detected), dataloss=False (risk)
assert_table_row(output, '/app/db', mount=True, ramdisk=False, dataloss=False)
elif test_scenario.name == 'db_no-mount':
# db not mounted: mount=False, dataloss=False (risk)
assert_table_row(output, '/app/db', mount=False, dataloss=False)
elif test_scenario.name == 'db_unwritable':
# db read-only: writeable=False
assert_table_row(output, '/app/db', writeable=False)
elif test_scenario.name.startswith('config_'):
if test_scenario.name == 'config_ramdisk':
# config on ramdisk: mount=True, ramdisk=False (detected), dataloss=False (risk)
assert_table_row(output, '/app/config', mount=True, ramdisk=False, dataloss=False)
elif test_scenario.name == 'config_no-mount':
# config not mounted: mount=False, dataloss=False (risk)
assert_table_row(output, '/app/config', mount=False, dataloss=False)
elif test_scenario.name == 'config_unwritable':
# config read-only: writeable=False
assert_table_row(output, '/app/config', writeable=False)
elif test_scenario.name.startswith('api_'):
if test_scenario.name == 'api_mounted':
# api with volume mount: mount=True, performance=False (not ramdisk)
assert_table_row(output, '/app/api', mount=True, performance=False)
elif test_scenario.name == 'api_no-mount':
# api not mounted: mount=False, performance=False (not ramdisk)
assert_table_row(output, '/app/api', mount=False, performance=False)
elif test_scenario.name == 'api_unwritable':
# api read-only: writeable=False
assert_table_row(output, '/app/api', writeable=False)
elif test_scenario.name.startswith('log_'):
if test_scenario.name == 'log_mounted':
# log with volume mount: mount=True, performance=False (not ramdisk)
assert_table_row(output, '/app/log', mount=True, performance=False)
elif test_scenario.name == 'log_no-mount':
# log not mounted: mount=False, performance=False (not ramdisk)
assert_table_row(output, '/app/log', mount=False, performance=False)
elif test_scenario.name == 'log_unwritable':
# log read-only: writeable=False
assert_table_row(output, '/app/log', writeable=False)
elif test_scenario.name.startswith('run_'):
if test_scenario.name == 'run_mounted':
# run with volume mount: mount=True, performance=False (not ramdisk)
assert_table_row(output, '/services/run', mount=True, performance=False)
elif test_scenario.name == 'run_no-mount':
# run not mounted: mount=False, performance=False (not ramdisk)
assert_table_row(output, '/services/run', mount=False, performance=False)
elif test_scenario.name == 'run_unwritable':
# run read-only: writeable=False
assert_table_row(output, '/services/run', writeable=False)
elif test_scenario.name.startswith('active_config_'):
if test_scenario.name == 'active_config_mounted':
# active_config with volume mount: mount=True, performance=False (not ramdisk)
assert_table_row(output, '/services/config/nginx/conf.active', mount=True, performance=False)
elif test_scenario.name == 'active_config_no-mount':
# active_config not mounted: mount=False, performance=False (not ramdisk)
assert_table_row(output, '/services/config/nginx/conf.active', mount=False, performance=False)
elif test_scenario.name == 'active_config_unwritable':
# active_config unwritable: RAM disk issues detected
assert_table_row(output, '/services/config/nginx/conf.active', ramdisk=False, performance=False)
except AssertionError as e:
pytest.fail(f"Table validation failed for {test_scenario.name}: {e}")
@pytest.mark.parametrize("test_scenario", create_test_scenarios(), ids=lambda s: s.name)
@pytest.mark.docker
def test_mount_diagnostic(netalertx_test_image, test_scenario):
"""Test that the mount diagnostic tool correctly identifies issues for each configuration."""
# Use the pre-generated docker-compose file
compose_file = CONFIG_DIR / "mount-tests" / test_scenario.docker_compose
assert compose_file.exists(), f"Docker compose file not found: {compose_file}"
# Start container
project_name = f"mount-test-{test_scenario.name.replace('_', '-')}"
# Remove any existing containers with the same project name
cmd_down = [
"docker-compose", "-f", str(compose_file),
"-p", project_name, "down", "-v"
]
subprocess.run(cmd_down, capture_output=True, timeout=30)
cmd_up = [
"docker-compose", "-f", str(compose_file),
"-p", project_name, "up", "-d"
]
result_up = subprocess.run(cmd_up, capture_output=True, text=True, timeout=60)
if result_up.returncode != 0:
pytest.fail(
f"Failed to start container: {result_up.stderr}\n"
f"STDOUT: {result_up.stdout}"
)
try:
# Wait for container to be ready
import time
time.sleep(4)
# Check if container is still running
container_name = f"netalertx-test-mount-{test_scenario.name}"
result_ps = subprocess.run(
["docker", "ps", "-q", "-f", f"name={container_name}"],
capture_output=True, text=True
)
if not result_ps.stdout.strip():
# Container exited - check the exit code
result_inspect = subprocess.run(
["docker", "inspect", container_name, "--format={{.State.ExitCode}}"],
capture_output=True, text=True
)
actual_exit_code = int(result_inspect.stdout.strip())
# Assert the exit code matches expected
assert actual_exit_code == test_scenario.expected_exit_code, (
f"Container {container_name} exited with code {actual_exit_code}, "
f"expected {test_scenario.expected_exit_code}"
)
# Check the logs to see if it detected the expected issues
result_logs = subprocess.run(
["docker", "logs", container_name],
capture_output=True, text=True
)
logs = result_logs.stdout + result_logs.stderr
if test_scenario.expected_issues:
validate_scenario_table_output(logs, test_scenario)
return # Test passed - container correctly detected issues and exited
# Container is still running - run diagnostic tool
cmd_exec = [
"docker", "exec", "--user", "netalertx", container_name,
"python3", "/entrypoint.d/10-mounts.py"
]
result_exec = subprocess.run(cmd_exec, capture_output=True, text=True, timeout=30)
diagnostic_output = result_exec.stdout + result_exec.stderr
# The diagnostic tool returns 1 for unwritable paths except active_config, which only warns
if test_scenario.name.startswith('active_config_') and 'unwritable' in test_scenario.name:
expected_tool_exit = 0
elif 'unwritable' in test_scenario.name:
expected_tool_exit = 1
else:
expected_tool_exit = 0
assert result_exec.returncode == expected_tool_exit, (
f"Diagnostic tool failed: {result_exec.stderr}"
)
if test_scenario.expected_issues:
validate_scenario_table_output(diagnostic_output, test_scenario)
assert "⚠️" in diagnostic_output, (
f"Issue scenario {test_scenario.name} should include a warning symbol, got: {result_exec.stderr}"
)
else:
# Should have table output but no warning message
assert "Path" in result_exec.stdout, f"Good config {test_scenario.name} should show table, got: {result_exec.stdout}"
assert "⚠️" not in diagnostic_output, (
f"Good config {test_scenario.name} should not show warning, got stderr: {result_exec.stderr}"
)
return # Test passed - diagnostic output validated
finally:
# Stop container
cmd_down = [
"docker-compose", "-f", str(compose_file),
"-p", project_name, "down", "-v"
]
subprocess.run(cmd_down, capture_output=True, timeout=30)
def test_table_parsing():
"""Test the table parsing and assertion functions."""
sample_output = """
Path | Writeable | Mount | RAMDisk | Performance | DataLoss
------------------------------------+-----------+-------+---------+-------------+----------
/app/db | ✅ | ❌ | | | ❌
/app/api | ✅ | ✅ | ✅ | ✅ | ✅
"""
# Test parsing
rows = parse_mount_table(sample_output)
assert len(rows) == 2
# Test assertions
assert_table_row(sample_output, "/app/db", writeable=True, mount=False, ramdisk=None, performance=None, dataloss=False)
assert_table_row(sample_output, "/app/api", writeable=True, mount=True, ramdisk=True, performance=True, dataloss=True)

View File

@@ -0,0 +1,240 @@
'''
Tests for 99-ports-available.sh entrypoint script.
This script checks for port conflicts and availability.
'''
import os
import pathlib
import subprocess
import time
import pytest
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
GRACE_SECONDS = float(os.environ.get("NETALERTX_TEST_GRACE", "2"))
VOLUME_MAP = {
"app_db": "/app/db",
"app_config": "/app/config",
"app_log": "/app/log",
"app_api": "/app/api",
"nginx_conf": "/services/config/nginx/conf.active",
"services_run": "/services/run",
}
pytestmark = [pytest.mark.docker, pytest.mark.feature_complete]
@pytest.fixture(scope="function")
def dummy_container(tmp_path):
"""Fixture that starts a dummy container to occupy ports for testing."""
# Create a simple docker-compose file for the dummy container
compose_file = tmp_path / "docker-compose-dummy.yml"
with open(compose_file, 'w') as f:
f.write("version: '3.8'\n")
f.write("services:\n")
f.write(" dummy:\n")
f.write(" image: alpine:latest\n")
f.write(" network_mode: host\n")
f.write(" userns_mode: host\n")
f.write(" command: sh -c \"while true; do nc -l -p 20211 < /dev/null > /dev/null; done & while true; do nc -l -p 20212 < /dev/null > /dev/null; done & sleep 30\"\n")
# Start the dummy container
import subprocess
result = subprocess.run(
["docker-compose", "-f", str(compose_file), "up", "-d"],
capture_output=True, text=True
)
if result.returncode != 0:
pytest.fail(f"Failed to start dummy container: {result.stderr}")
# Wait a bit for the container to start listening
time.sleep(3)
yield "dummy"
# Cleanup
subprocess.run(["docker-compose", "-f", str(compose_file), "down"], capture_output=True)
def _setup_mount_tree(tmp_path: pathlib.Path, label: str) -> dict[str, pathlib.Path]:
"""Set up mount tree for testing."""
import uuid
import shutil
base = tmp_path / f"{label}_mount_root"
if base.exists():
shutil.rmtree(base)
base.mkdir(parents=True)
paths = {}
for key, target in VOLUME_MAP.items():
folder_name = f"{label}_{key.upper()}_INTENTIONAL_NETALERTX_TEST"
host_path = base / folder_name
host_path.mkdir(parents=True, exist_ok=True)
host_path.chmod(0o777)
paths[key] = host_path
return paths
def _build_volume_args(paths: dict[str, pathlib.Path]) -> list[tuple[str, str, bool]]:
"""Build volume arguments for docker run."""
bindings = []
for key, target in VOLUME_MAP.items():
bindings.append((str(paths[key]), target, False))
return bindings
def _run_container(
label: str,
volumes: list[tuple[str, str, bool]] | None = None,
*,
env: dict[str, str] | None = None,
user: str | None = None,
network_mode: str | None = "host",
extra_args: list[str] | None = None,
) -> subprocess.CompletedProcess[str]:
"""Run a container and return the result."""
import uuid
import re
name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower()
cmd = ["docker", "run", "--rm", "--name", name]
if network_mode:
cmd.extend(["--network", network_mode])
cmd.extend(["--userns", "host"])
cmd.extend(["--tmpfs", "/tmp:mode=777"])
if user:
cmd.extend(["--user", user])
if env:
for key, value in env.items():
cmd.extend(["-e", f"{key}={value}"])
if extra_args:
cmd.extend(extra_args)
for host_path, target, readonly in volumes or []:
mount = f"{host_path}:{target}"
if readonly:
mount += ":ro"
cmd.extend(["-v", mount])
# Copy the script content and run it
script_path = pathlib.Path("install/production-filesystem/entrypoint.d/99-ports-available.sh")
with script_path.open('r', encoding='utf-8') as f:
script_content = f.read()
# Use printf to avoid shell interpretation issues
script = f"printf '%s\\n' '{script_content.replace(chr(39), chr(39)+chr(92)+chr(39)+chr(39))}' > /tmp/ports-check.sh && chmod +x /tmp/ports-check.sh && sh /tmp/ports-check.sh"
cmd.extend(["--entrypoint", "/bin/sh", IMAGE, "-c", script])
print(f"\n--- DOCKER CMD ---\n{' '.join(cmd)}\n--- END CMD ---\n")
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=30,
check=False,
)
# Combine and clean stdout and stderr
stdouterr = (
re.sub(r'\x1b\[[0-9;]*m', '', result.stdout or '') +
re.sub(r'\x1b\[[0-9;]*m', '', result.stderr or '')
)
result.output = stdouterr
print(f"\n--- CONTAINER stdout ---\n{result.stdout}")
print(f"\n--- CONTAINER stderr ---\n{result.stderr}")
print(f"\n--- CONTAINER combined ---\n{result.output}")
return result
def _assert_contains(result, snippet: str, cmd: list[str] = None) -> None:
"""Assert that the result output contains the given snippet."""
if snippet not in result.output:
cmd_str = " ".join(cmd) if cmd else ""
raise AssertionError(
f"Expected to find '{snippet}' in container output.\n"
f"Got:\n{result.output}\n"
f"Container command:\n{cmd_str}"
)
def _assert_not_contains(result, snippet: str, cmd: list[str] = None) -> None:
"""Assert that the result output does not contain the given snippet."""
if snippet in result.output:
cmd_str = " ".join(cmd) if cmd else ""
raise AssertionError(
f"Expected NOT to find '{snippet}' in container output.\n"
f"Got:\n{result.output}\n"
f"Container command:\n{cmd_str}"
)
def test_ports_available_normal_case(tmp_path: pathlib.Path) -> None:
"""Test ports available script with default ports (should pass without warnings).
99. Ports Available Check: Tests that the script runs without warnings
when ports 20211 and 20212 are available and not conflicting.
Expected: No warnings about port conflicts or ports in use.
Check script: 99-ports-available.sh
"""
paths = _setup_mount_tree(tmp_path, "ports_normal")
volumes = _build_volume_args(paths)
result = _run_container("ports-normal", volumes, user="20211:20211", env={"PORT": "99991", "GRAPHQL_PORT": "99992"})
# Should not contain any port warnings
_assert_not_contains(result, "Configuration Warning: Both ports are set to")
_assert_not_contains(result, "Port Warning: Application port")
_assert_not_contains(result, "Port Warning: GraphQL API port")
assert result.returncode == 0
def test_ports_conflict_same_number(tmp_path: pathlib.Path) -> None:
"""Test ports available script when both ports are set to the same number.
99. Ports Available Check: Tests warning when PORT and GRAPHQL_PORT
are configured to the same value.
Expected: Warning about port conflict.
Check script: 99-ports-available.sh
"""
paths = _setup_mount_tree(tmp_path, "ports_conflict")
volumes = _build_volume_args(paths)
result = _run_container(
"ports-conflict",
volumes,
user="20211:20211",
env={"PORT": "20211", "GRAPHQL_PORT": "20211"}
)
_assert_contains(result, "Configuration Warning: Both ports are set to 20211")
_assert_contains(result, "The Application port ($PORT) and the GraphQL API port")
_assert_contains(result, "are configured to use the")
_assert_contains(result, "same port. This will cause a conflict.")
assert result.returncode == 0
def test_ports_in_use_warning(dummy_container, tmp_path: pathlib.Path) -> None:
"""Test ports available script when ports are already in use.
99. Ports Available Check: Tests warning when configured ports
are already bound by another process.
Expected: Warning about ports being in use.
Check script: 99-ports-available.sh
"""
paths = _setup_mount_tree(tmp_path, "ports_in_use")
volumes = _build_volume_args(paths)
result = _run_container(
"ports-in-use",
volumes,
user="20211:20211",
env={"PORT": "20211", "GRAPHQL_PORT": "20212"}
)
_assert_contains(result, "Port Warning: Application port 20211 is already in use")
_assert_contains(result, "Port Warning: GraphQL API port 20212 is already in use")
assert result.returncode == 0