Compare commits
53 Commits
d3326b3362
...
mcp-endpoi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f25560c8 | ||
|
|
fc4d32ebe7 | ||
|
|
b47325d06a | ||
|
|
436ac6de49 | ||
|
|
c1bd611e57 | ||
|
|
edde2596b5 | ||
|
|
da9d37c718 | ||
|
|
5bcb727305 | ||
|
|
2dc688b16c | ||
|
|
0ac9fd79b3 | ||
|
|
3d17dc47b5 | ||
|
|
ef2e7886c4 | ||
|
|
c8f3a84b92 | ||
|
|
9688fee2d2 | ||
|
|
2dcd9eda19 | ||
|
|
24187495e1 | ||
|
|
c27d25d4ab | ||
|
|
93a2dad2eb | ||
|
|
b235863644 | ||
|
|
f387f8c5b6 | ||
|
|
5af760f5ee | ||
|
|
d93a3981fa | ||
|
|
fbb4a2f8b4 | ||
|
|
54bce6505b | ||
|
|
6da47cc830 | ||
|
|
9cabbf3622 | ||
|
|
6c28a08bee | ||
|
|
86e3decd4e | ||
|
|
e14e0bb9e8 | ||
|
|
b6023d1373 | ||
|
|
1812cc8ef8 | ||
|
|
5df39f984a | ||
|
|
d007ed711a | ||
|
|
61824abb9f | ||
|
|
33c5548fe1 | ||
|
|
fd41c395ae | ||
|
|
1a980844f0 | ||
|
|
82e018e284 | ||
|
|
e0e1233b1c | ||
|
|
74677f940e | ||
|
|
21a4d20579 | ||
|
|
9634e4e0f7 | ||
|
|
00a47ab5d3 | ||
|
|
59b417705e | ||
|
|
525d082f3d | ||
|
|
ba3481759b | ||
|
|
7125cea29b | ||
|
|
8586c5a307 | ||
|
|
0d81315809 | ||
|
|
8f193f1e2c | ||
|
|
b1eef8aa09 | ||
|
|
2da17f272c | ||
|
|
7bcb4586b2 |
6
.github/workflows/docker_dev.yml
vendored
@@ -47,6 +47,12 @@ jobs:
|
||||
id: get_version
|
||||
run: echo "version=Dev" >> $GITHUB_OUTPUT
|
||||
|
||||
# --- debug output
|
||||
- name: Debug version
|
||||
run: |
|
||||
echo "GITHUB_REF: $GITHUB_REF"
|
||||
echo "Version: '${{ steps.get_version.outputs.version }}'"
|
||||
|
||||
# --- Write the timestamped version to .VERSION file
|
||||
- name: Create .VERSION file
|
||||
run: echo "${{ steps.timestamp.outputs.version }}" > .VERSION
|
||||
|
||||
22
.github/workflows/docker_prod.yml
vendored
@@ -32,14 +32,34 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# --- Previous approach Get release version from tag
|
||||
- name: Set up dynamic build ARGs
|
||||
id: getargs
|
||||
run: echo "version=$(cat ./stable/VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get release version
|
||||
id: get_version_prev
|
||||
run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}"
|
||||
|
||||
- name: Create .VERSION file
|
||||
run: echo "${{ steps.get_version.outputs.version }}" >> .VERSION_PREV
|
||||
|
||||
# --- Get release version from tag
|
||||
- name: Get release version
|
||||
id: get_version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# --- debug output
|
||||
- name: Debug version
|
||||
run: |
|
||||
echo "GITHUB_REF: $GITHUB_REF"
|
||||
echo "Version: '${{ steps.get_version.outputs.version }}'"
|
||||
echo "Version prev: '${{ steps.get_version_prev.outputs.version }}'"
|
||||
|
||||
# --- Write version to .VERSION file
|
||||
- name: Create .VERSION file
|
||||
run: echo "${{ steps.get_version.outputs.version }}" > .VERSION
|
||||
run: echo -n "${{ steps.get_version.outputs.version }}" > .VERSION
|
||||
|
||||
# --- Generate Docker metadata and tags
|
||||
- name: Docker meta
|
||||
|
||||
1
.gitignore
vendored
@@ -11,6 +11,7 @@ nohup.out
|
||||
config/*
|
||||
.ash_history
|
||||
.VERSION
|
||||
.VERSION_PREV
|
||||
config/pialert.conf
|
||||
config/app.conf
|
||||
db/*
|
||||
|
||||
29
Dockerfile
@@ -1,16 +1,16 @@
|
||||
# The NetAlertX Dockerfile has 3 stages:
|
||||
#
|
||||
# Stage 1. Builder - NetAlertX Requires special tools and packages to build our virtual environment, but
|
||||
# which are not needed in future stages. We build the builder and extract the venv for runner to use as
|
||||
# which are not needed in future stages. We build the builder and extract the venv for runner to use as
|
||||
# a base.
|
||||
#
|
||||
# Stage 2. Runner builds the bare minimum requirements to create an operational NetAlertX. The primary
|
||||
# reason for breaking at this stage is it leaves the system in a proper state for devcontainer operation
|
||||
# This image also provides a break-out point for uses who wish to execute the anti-pattern of using a
|
||||
# This image also provides a break-out point for uses who wish to execute the anti-pattern of using a
|
||||
# docker container as a VM for experimentation and various development patterns.
|
||||
#
|
||||
# Stage 3. Hardened removes root, sudoers, folders, permissions, and locks the system down into a read-only
|
||||
# compatible image. While NetAlertX does require some read-write operations, this image can guarantee the
|
||||
# compatible image. While NetAlertX does require some read-write operations, this image can guarantee the
|
||||
# code pushed out by the project is the only code which will run on the system after each container restart.
|
||||
# It reduces the chance of system hijacking and operates with all modern security protocols in place as is
|
||||
# expected from a security appliance.
|
||||
@@ -29,7 +29,7 @@ COPY requirements.txt /tmp/requirements.txt
|
||||
RUN apk add --no-cache bash shadow python3 python3-dev gcc musl-dev libffi-dev openssl-dev git \
|
||||
&& python -m venv /opt/venv
|
||||
|
||||
# Create virtual environment owned by root, but readable by everyone else. This makes it easy to copy
|
||||
# Create virtual environment owned by root, but readable by everyone else. This makes it easy to copy
|
||||
# into hardened stage without worrying about permissions and keeps image size small. Keeping the commands
|
||||
# together makes for a slightly smaller image size.
|
||||
RUN pip install --no-cache-dir -r /tmp/requirements.txt && \
|
||||
@@ -95,11 +95,11 @@ ENV READ_WRITE_FOLDERS="${NETALERTX_DATA} ${NETALERTX_CONFIG} ${NETALERTX_DB} ${
|
||||
${SYSTEM_SERVICES_ACTIVE_CONFIG}"
|
||||
|
||||
#Python environment
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV VIRTUAL_ENV_BIN=/opt/venv/bin
|
||||
ENV PYTHONPATH=${NETALERTX_APP}:${NETALERTX_SERVER}:${NETALERTX_PLUGINS}:${VIRTUAL_ENV}/lib/python3.12/site-packages
|
||||
ENV PATH="${SYSTEM_SERVICES}:${VIRTUAL_ENV_BIN}:$PATH"
|
||||
ENV PATH="${SYSTEM_SERVICES}:${VIRTUAL_ENV_BIN}:$PATH"
|
||||
|
||||
# App Environment
|
||||
ENV LISTEN_ADDR=0.0.0.0
|
||||
@@ -110,7 +110,7 @@ ENV VENDORSPATH_NEWEST=${SYSTEM_SERVICES_RUN_TMP}/ieee-oui.txt
|
||||
ENV ENVIRONMENT=alpine
|
||||
ENV READ_ONLY_USER=readonly READ_ONLY_GROUP=readonly
|
||||
ENV NETALERTX_USER=netalertx NETALERTX_GROUP=netalertx
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
|
||||
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap \
|
||||
@@ -138,6 +138,7 @@ RUN install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 ${READ_WRITE_FO
|
||||
|
||||
# Copy version information into the image
|
||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION
|
||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION_PREV
|
||||
|
||||
# Copy the virtualenv from the builder stage
|
||||
COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
@@ -147,12 +148,12 @@ COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
# This is done after the copy of the venv to ensure the venv is in place
|
||||
# although it may be quicker to do it before the copy, it keeps the image
|
||||
# layers smaller to do it after.
|
||||
RUN if [ -f '.VERSION' ]; then \
|
||||
cp '.VERSION' "${NETALERTX_APP}/.VERSION"; \
|
||||
else \
|
||||
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/.VERSION"; \
|
||||
fi && \
|
||||
chown 20212:20212 "${NETALERTX_APP}/.VERSION" && \
|
||||
RUN for vfile in .VERSION .VERSION_PREV; do \
|
||||
if [ ! -f "${NETALERTX_APP}/${vfile}" ]; then \
|
||||
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/${vfile}"; \
|
||||
fi; \
|
||||
chown 20212:20212 "${NETALERTX_APP}/${vfile}"; \
|
||||
done && \
|
||||
apk add --no-cache libcap && \
|
||||
setcap cap_net_raw+ep /bin/busybox && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
|
||||
@@ -180,7 +181,7 @@ ENV UMASK=0077
|
||||
|
||||
# Create readonly user and group with no shell access.
|
||||
# Readonly user marks folders that are created by NetAlertX, but should not be modified.
|
||||
# AI may claim this is stupid, but it's actually least possible permissions as
|
||||
# AI may claim this is stupid, but it's actually least possible permissions as
|
||||
# read-only user cannot login, cannot sudo, has no write permission, and cannot even
|
||||
# read the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
|
||||
RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
|
||||
|
||||
@@ -34,9 +34,7 @@ Get visibility of what's going on on your WIFI/LAN network and enable presence d
|
||||
## 🚀 Quick Start
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ **Important:** The documentation has been recently updated and some instructions may have changed.
|
||||
> If you are using the currently live production image, please follow the instructions on [Docker Hub](https://hub.docker.com/r/jokobsk/netalertx) for building and running the container.
|
||||
> These docs reflect the latest development version and may differ from the production image.
|
||||
> ⚠️ **Important:** The docker-compose has recently changed. Carefully read the [Migration guide](https://jokob-sk.github.io/NetAlertX/MIGRATION/?h=migrat#12-migration-from-netalertx-v25524) for detailed instructions.
|
||||
|
||||
Start NetAlertX in seconds with Docker:
|
||||
|
||||
@@ -44,8 +42,7 @@ Start NetAlertX in seconds with Docker:
|
||||
docker run -d \
|
||||
--network=host \
|
||||
--restart unless-stopped \
|
||||
-v /local_data_dir/config:/data/config \
|
||||
-v /local_data_dir/db:/data/db \
|
||||
-v /local_data_dir:/data \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
-e PORT=20211 \
|
||||
@@ -53,6 +50,8 @@ docker run -d \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
```
|
||||
|
||||
Note: Your `/local_data_dir` should contain a `config` and `db` folder.
|
||||
|
||||
To deploy a containerized instance directly from the source repository, execute the following BASH sequence:
|
||||
```bash
|
||||
git clone https://github.com/jokob-sk/NetAlertX.git
|
||||
|
||||
@@ -112,3 +112,11 @@ Slowness can be caused by:
|
||||
|
||||
> See [Performance Tips](./PERFORMANCE.md) for detailed optimization steps.
|
||||
|
||||
|
||||
#### IP flipping
|
||||
|
||||
With `ARPSCAN` scans some devices might flip IP addresses after each scan triggering false notifications. This is because some devices respond to broadcast calls and thus different IPs after scans are logged.
|
||||
|
||||
See how to prevent IP flipping in the [ARPSCAN plugin guide](/front/plugins/arp_scan/README.md).
|
||||
|
||||
Alternatively adjust your [notification settings](./NOTIFICATIONS.md) to prevent false positives by filtering out events or devices.
|
||||
|
||||
@@ -16,8 +16,7 @@ Start the container via the **terminal** with a command similar to this one:
|
||||
docker run \
|
||||
--network=host \
|
||||
--restart unless-stopped \
|
||||
-v /local_data_dir/config:/data/config \
|
||||
-v /local_data_dir/db:/data/db \
|
||||
-v /local_data_dir:/data \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
-e PORT=20211 \
|
||||
@@ -26,6 +25,8 @@ docker run \
|
||||
|
||||
```
|
||||
|
||||
Note: Your `/local_data_dir` should contain a `config` and `db` folder.
|
||||
|
||||
> [!NOTE]
|
||||
> ⚠ The most important part is NOT to use the `-d` parameter so you see the error when the container crashes. Use this error in your issue description.
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
# NetAlertX and Docker Compose
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ **Important:** The documentation has been recently updated and some instructions may have changed.
|
||||
> If you are using the currently live production image, please follow the instructions on [Docker Hub](https://hub.docker.com/r/jokobsk/netalertx) for building and running the container.
|
||||
> These docs reflect the latest development version and may differ from the production image.
|
||||
> ⚠️ **Important:** The docker-compose has recently changed. Carefully read the [Migration guide](https://jokob-sk.github.io/NetAlertX/MIGRATION/?h=migrat#12-migration-from-netalertx-v25524) for detailed instructions.
|
||||
|
||||
Great care is taken to ensure NetAlertX meets the needs of everyone while being flexible enough for anyone. This document outlines how you can configure your docker-compose. There are many settings, so we recommend using the Baseline Docker Compose as-is, or modifying it for your system.Good care is taken to ensure NetAlertX meets the needs of everyone while being flexible enough for anyone. This document outlines how you can configure your docker-compose. There are many settings, so we recommend using the Baseline Docker Compose as-is, or modifying it for your system.
|
||||
Great care is taken to ensure NetAlertX meets the needs of everyone while being flexible enough for anyone. This document outlines how you can configure your docker-compose. There are many settings, so we recommend using the Baseline Docker Compose as-is, or modifying it for your system.Good care is taken to ensure NetAlertX meets the needs of everyone while being flexible enough for anyone. This document outlines how you can configure your docker-compose. There are many settings, so we recommend using the Baseline Docker Compose as-is, or modifying it for your system.
|
||||
|
||||
> [!NOTE]
|
||||
> The container needs to run in `network_mode:"host"` to access Layer 2 networking such as arp, nmap and others. Due to lack of support for this feature, Windows host is not a supported operating system.
|
||||
> [!NOTE]
|
||||
> The container needs to run in `network_mode:"host"` to access Layer 2 networking such as arp, nmap and others. Due to lack of support for this feature, Windows host is not a supported operating system.
|
||||
|
||||
## Baseline Docker Compose
|
||||
|
||||
There is one baseline for NetAlertX. That's the default security-enabled official distribution.
|
||||
There is one baseline for NetAlertX. That's the default security-enabled official distribution.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -45,7 +43,7 @@ services:
|
||||
# - /home/user/netalertx_data:/data:rw
|
||||
|
||||
- type: bind # Bind mount for timezone consistency
|
||||
source: /etc/localtime
|
||||
source: /etc/localtime
|
||||
target: /etc/localtime
|
||||
read_only: true
|
||||
|
||||
@@ -125,9 +123,9 @@ docker compose up
|
||||
|
||||
### Modification 1: Use a Local Folder (Bind Mount)
|
||||
|
||||
By default, the baseline compose file uses a single named volume (netalertx_data) mounted at /data. This single-volume layout is preferred because NetAlertX manages both configuration and the database under /data (for example, /data/config and /data/db) via its web UI. Using one named volume simplifies permissions and portability: Docker manages the storage and NetAlertX manages the files inside /data.
|
||||
By default, the baseline compose file uses a single named volume (netalertx_data) mounted at `/data`. This single-volume layout is preferred because NetAlertX manages both configuration and the database under `/data` (for example, `/data/config` and `/data/db`) via its web UI. Using one named volume simplifies permissions and portability: Docker manages the storage and NetAlertX manages the files inside `/data`.
|
||||
|
||||
A two-volume layout that mounts /data/config and /data/db separately (for example, netalertx_config and netalertx_db) is supported for backward compatibility and some advanced workflows, but it is an abnormal/legacy layout and not recommended for new deployments.
|
||||
A two-volume layout that mounts `/data/config` and `/data/db` separately (for example, `netalertx_config` and `netalertx_db`) is supported for backward compatibility and some advanced workflows, but it is an abnormal/legacy layout and not recommended for new deployments.
|
||||
|
||||
However, if you prefer to have direct, file-level access to your configuration for manual editing, a "bind mount" is a simple alternative. This tells Docker to use a specific folder from your computer (the "host") inside the container.
|
||||
|
||||
@@ -187,7 +185,7 @@ services:
|
||||
environment:
|
||||
- PORT=${PORT}
|
||||
- GRAPHQL_PORT=${GRAPHQL_PORT}
|
||||
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ Head to [https://netalertx.com/](https://netalertx.com/) for more gifs and scree
|
||||
|
||||
```bash
|
||||
docker run -d --rm --network=host \
|
||||
-v /local_data_dir/config:/data/config \
|
||||
-v /local_data_dir/db:/data/db \
|
||||
-v /local_data_dir:/data \
|
||||
-v /etc/localtime:/etc/localtime \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
-e PORT=20211 \
|
||||
@@ -62,21 +61,38 @@ See alternative [docked-compose examples](https://github.com/jokob-sk/NetAlertX/
|
||||
|
||||
| Required | Path | Description |
|
||||
| :------------- | :------------- | :-------------|
|
||||
| ✅ | `:/data/config` | Folder which will contain the `app.conf` & `devices.csv` ([read about devices.csv](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEVICES_BULK_EDITING.md)) files |
|
||||
| ✅ | `:/data/db` | Folder which will contain the `app.db` database file |
|
||||
| ✅ | `/etc/localtime:/etc/localtime:ro` | Ensuring the timezone is teh same as on teh server. |
|
||||
| ✅ | `:/data` | Folder which needs to contain a `/db` and `/config` sub-folders. |
|
||||
| ✅ | `/etc/localtime:/etc/localtime:ro` | Ensuring the timezone is the same as on the server. |
|
||||
| | `:/tmp/log` | Logs folder useful for debugging if you have issues setting up the container |
|
||||
| | `:/tmp/api` | The [API endpoint](https://github.com/jokob-sk/NetAlertX/blob/main/docs/API.md) containing static (but regularly updated) json and other files. Path configurable via `NETALERTX_API` environment variable. |
|
||||
| | `:/app/front/plugins/<plugin>/ignore_plugin` | Map a file `ignore_plugin` to ignore a plugin. Plugins can be soft-disabled via settings. More in the [Plugin docs](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md). |
|
||||
| | `:/etc/resolv.conf` | Use a custom `resolv.conf` file for [better name resolution](https://github.com/jokob-sk/NetAlertX/blob/main/docs/REVERSE_DNS.md). |
|
||||
|
||||
> Use separate `db` and `config` directories, do not nest them.
|
||||
### Folder structure
|
||||
|
||||
Use separate `db` and `config` directories, do not nest them:
|
||||
|
||||
```
|
||||
data
|
||||
├── config
|
||||
└── db
|
||||
```
|
||||
|
||||
### Permissions
|
||||
|
||||
If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the `/local_data_dir/db` and `/local_data_dir/config` folders (replace `local_data_dir` with the location where your `/db` and `/config` folders are located).
|
||||
|
||||
```bash
|
||||
sudo chown -R 20211:20211 /local_data_dir
|
||||
sudo chmod -R a+rwx /local_data_dir
|
||||
```
|
||||
|
||||
### Initial setup
|
||||
|
||||
- If unavailable, the app generates a default `app.conf` and `app.db` file on the first run.
|
||||
- The preferred way is to manage the configuration via the Settings section in the UI, if UI is inaccessible you can modify [app.conf](https://github.com/jokob-sk/NetAlertX/tree/main/back) in the `/data/config/` folder directly
|
||||
|
||||
|
||||
#### Setting up scanners
|
||||
|
||||
You have to specify which network(s) should be scanned. This is done by entering subnets that are accessible from the host. If you use the default `ARPSCAN` plugin, you have to specify at least one valid subnet and interface in the `SCAN_SUBNETS` setting. See the documentation on [How to set up multiple SUBNETS, VLANs and what are limitations](https://github.com/jokob-sk/NetAlertX/blob/main/docs/SUBNETS.md) for troubleshooting and more advanced scenarios.
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
# The NetAlertX Container Operator's Guide
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ **Important:** The documentation has been recently updated and some instructions may have changed.
|
||||
> If you are using the currently live production image, please follow the instructions on [Docker Hub](https://hub.docker.com/r/jokobsk/netalertx) for building and running the container.
|
||||
> These docs reflect the latest development version and may differ from the production image.
|
||||
> ⚠️ **Important:** The docker-compose has recently changed. Carefully read the [Migration guide](https://jokob-sk.github.io/NetAlertX/MIGRATION/?h=migrat#12-migration-from-netalertx-v25524) for detailed instructions.
|
||||
|
||||
This guide assumes you are starting with the official `docker-compose.yml` file provided with the project. We strongly recommend you start with or migrate to this file as your baseline and modify it to suit your specific needs (e.g., changing file paths). While there are many ways to configure NetAlertX, the default file is designed to meet the mandatory security baseline with layer-2 networking capabilities while operating securely and without startup warnings.
|
||||
|
||||
This guide provides direct, concise solutions for common NetAlertX administrative tasks. It is structured to help you identify a problem, implement the solution, and understand the details.
|
||||
|
||||
## Guide Contents
|
||||
|
||||
- Using a Local Folder for Configuration
|
||||
- Migrating from a Local Folder to a Docker Volume
|
||||
- Applying a Custom Nginx Configuration
|
||||
- Mounting Additional Files for Plugins
|
||||
|
||||
- Using a Local Folder for Configuration
|
||||
- Migrating from a Local Folder to a Docker Volume
|
||||
- Applying a Custom Nginx Configuration
|
||||
- Mounting Additional Files for Plugins
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -78,7 +78,7 @@ In the **Environment variables** section of Portainer, add the following:
|
||||
>
|
||||
> `sudo chown -R 20211:20211 /local_data_dir`
|
||||
>
|
||||
> `sudo chmod -R a+rwx /local_data_dir1`
|
||||
> `sudo chmod -R a+rwx /local_data_dir`
|
||||
>
|
||||
|
||||
|
||||
|
||||
@@ -46,8 +46,7 @@ NetAlertX requires certain paths to be writable at runtime. These paths should b
|
||||
|
||||
```bash
|
||||
docker run -it --rm --name netalertx --user "0" \
|
||||
-v /local_data_dir/config:/data/config \
|
||||
-v /local_data_dir/db:/data/db \
|
||||
-v /local_data_dir:/data \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
```
|
||||
@@ -63,7 +62,7 @@ docker run -it --rm --name netalertx --user "0" \
|
||||
>
|
||||
> `sudo chown -R 20211:20211 /local_data_dir`
|
||||
>
|
||||
> `sudo chmod -R a+rwx /local_data_dir1`
|
||||
> `sudo chmod -R a+rwx /local_data_dir`
|
||||
>
|
||||
|
||||
---
|
||||
@@ -84,8 +83,7 @@ services:
|
||||
- NET_BIND_SERVICE # Required to bind to privileged ports (nbtscan)
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /local_data_dir/config:/data/config
|
||||
- /local_data_dir/db:/data/db
|
||||
- /local_data_dir:/data
|
||||
- /etc/localtime:/etc/localtime
|
||||
environment:
|
||||
- PORT=20211
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# Migration
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ **Important:** The documentation has been recently updated and some instructions may have changed.
|
||||
> If you are using the currently live production image, please follow the instructions on [Docker Hub](https://hub.docker.com/r/jokobsk/netalertx) for building and running the container.
|
||||
> These docs reflect the latest development version and may differ from the production image.
|
||||
|
||||
|
||||
When upgrading from older versions of NetAlertX (or PiAlert by jokob-sk), follow the migration steps below to ensure your data and configuration are properly transferred.
|
||||
|
||||
> [!TIP]
|
||||
@@ -245,30 +239,7 @@ services:
|
||||
|
||||
4. Start the container and verify everything works as expected.
|
||||
5. Stop the container.
|
||||
6. Perform a one-off migration to the latest `netalertx` image and `20211` user:
|
||||
|
||||
> [!NOTE]
|
||||
> The example below assumes your `/config` and `/db` folders are stored in `local_data_dir`.
|
||||
> Replace this path with your actual configuration directory. `netalertx` is the container name, which might differ from your setup.
|
||||
|
||||
```sh
|
||||
docker run -it --rm --name netalertx --user "0" \
|
||||
-v /local_data_dir/config:/data/config \
|
||||
-v /local_data_dir/db:/data/db \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
```
|
||||
|
||||
..or alternatively execute:
|
||||
|
||||
```bash
|
||||
sudo chown -R 20211:20211 /local_data_dir/config
|
||||
sudo chown -R 20211:20211 /local_data_dir/db
|
||||
sudo chmod -R a+rwx /local_data_dir/
|
||||
```
|
||||
|
||||
7. Stop the container
|
||||
8. Update the `docker-compose.yml` as per example below.
|
||||
6. Update the `docker-compose.yml` as per example below.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -284,8 +255,7 @@ services:
|
||||
- NET_BIND_SERVICE # 🆕 New line
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /local_data_dir/config:/data/config # 🆕 This has changed from /app to /data
|
||||
- /local_data_dir/db:/data/db # 🆕 This has changed from /app to /data
|
||||
- /local_data_dir:/data # 🆕 This folder contains your /db and /config directories and the parent changed from /app to /data
|
||||
# Ensuring the timezone is the same as on the server - make sure also the TIMEZONE setting is configured
|
||||
- /etc/localtime:/etc/localtime:ro # 🆕 New line
|
||||
environment:
|
||||
@@ -296,5 +266,36 @@ services:
|
||||
- "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||
# 🆕 New "tmpfs" section END 🔼
|
||||
```
|
||||
7. Perform a one-off migration to the latest `netalertx` image and `20211` user.
|
||||
|
||||
9. Start the container and verify everything works as expected.
|
||||
> [!NOTE]
|
||||
> The examples below assumes your `/config` and `/db` folders are stored in `local_data_dir`.
|
||||
> Replace this path with your actual configuration directory. `netalertx` is the container name, which might differ from your setup.
|
||||
|
||||
**Automated approach**:
|
||||
|
||||
Run the container with the `--user "0"` parameter. Please note, some systems will require the manual approach below.
|
||||
|
||||
```sh
|
||||
docker run -it --rm --name netalertx --user "0" \
|
||||
-v /local_data_dir/config:/app/config \
|
||||
-v /local_data_dir/db:/app/db \
|
||||
-v /local_data_dir:/data \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
```
|
||||
|
||||
Stop the container and run it as you would normally.
|
||||
|
||||
**Manual approach**:
|
||||
|
||||
Use the manual approach if the Automated approach fails. Execute the below commands:
|
||||
|
||||
```bash
|
||||
sudo chown -R 20211:20211 /local_data_dir
|
||||
sudo chmod -R a+rwx /local_data_dir
|
||||
```
|
||||
|
||||
8. Start the container and verify everything works as expeexpected.
|
||||
9. Check the [Permissions -> Writable-paths](https://jokob-sk.github.io/NetAlertX/FILE_PERMISSIONS/#writable-paths) what directories to mount if you'd like to access the API or log files.
|
||||
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
# Integration with PiHole
|
||||
|
||||
NetAlertX comes with 2 plugins suitable for integrating with your existing PiHole instance. One plugin is using a direct SQLite DB connection, the other leverages the DHCP.leases file generated by PiHole. You can combine both approaches and also supplement it with other [plugins](/docs/PLUGINS.md).
|
||||
NetAlertX comes with 3 plugins suitable for integrating with your existing PiHole instance. The first plugin uses the v6 API, the second plugin is using a direct SQLite DB connection, the other leverages the `DHCP.leases` file generated by PiHole. You can combine multiple approaches and also supplement scans with other [plugins](/docs/PLUGINS.md).
|
||||
|
||||
## Approach 1: `DHCPLSS` Plugin - Import devices from the PiHole DHCP leases file
|
||||
## Approach 1: `PIHOLEAPI` Plugin - Import devices directly from PiHole v6 API
|
||||
|
||||

|
||||
|
||||
To use this approach make sure the Web UI password in **Pi-hole** is set.
|
||||
|
||||
| Setting | Description | Recommended value |
|
||||
| :------------- | :------------- | :-------------|
|
||||
| `PIHOLEAPI_URL` | Your Pi-hole base URL including port. | `http://192.168.1.82:9880/` |
|
||||
| `PIHOLEAPI_RUN_SCHD` | If you run multiple device scanner plugins, align the schedules of all plugins to the same value. | `*/5 * * * *` |
|
||||
| `PIHOLEAPI_PASSWORD` | The Web UI base64 encoded (en-/decoding handled by the app) admin password. | `passw0rd` |
|
||||
| `PIHOLEAPI_SSL_VERIFY` | Whether to verify HTTPS certificates. Disable only for self-signed certificates. | `False` |
|
||||
| `PIHOLEAPI_API_MAXCLIENTS` | Maximum number of devices to request from Pi-hole. Defaults are usually fine. | `500` |
|
||||
| `PIHOLEAPI_FAKE_MAC` | Generate FAKE MAC from IP. | `False` |
|
||||
|
||||
Check the [PiHole API plugin readme](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/pihole_api_scan/) for details and troubleshooting.
|
||||
|
||||
### docker-compose changes
|
||||
|
||||
No changes needed
|
||||
|
||||
## Approach 2: `DHCPLSS` Plugin - Import devices from the PiHole DHCP leases file
|
||||
|
||||

|
||||
|
||||
@@ -23,12 +44,12 @@ Check the [DHCPLSS plugin readme](https://github.com/jokob-sk/NetAlertX/tree/mai
|
||||
| `:/etc/pihole/dhcp.leases` | PiHole's `dhcp.leases` file. Required if you want to use PiHole `dhcp.leases` file. This has to be matched with a corresponding `DHCPLSS_paths_to_check` setting entry (the path in the container must contain `pihole`) |
|
||||
|
||||
|
||||
## Approach 2: `PIHOLE` Plugin - Import devices directly from the PiHole database
|
||||
## Approach 3: `PIHOLE` Plugin - Import devices directly from the PiHole database
|
||||
|
||||

|
||||
|
||||
| Setting | Description | Recommended value |
|
||||
| :------------- | :------------- | :-------------|
|
||||
| :------------- | :------------- | :-------------|
|
||||
| `PIHOLE_RUN` | When the plugin should run. | `schedule` |
|
||||
| `PIHOLE_RUN_SCHD` | If you run multiple device scanner plugins, align the schedules of all plugins to the same value. | `*/5 * * * *` |
|
||||
| `PIHOLE_DB_PATH` | You need to map the value in this setting in the `docker-compose.yml` file. | `/etc/pihole/pihole-FTL.db` |
|
||||
|
||||
@@ -13,13 +13,13 @@ There is also an in-app Help / FAQ section that should be answering frequently a
|
||||
|
||||
#### 🐳 Docker (Fully supported)
|
||||
|
||||
- The main installation method is as a [docker container - follow these instructions here](./DOCKER_INSTALLATION.md).
|
||||
- The main installation method is as a [docker container - follow these instructions here](./DOCKER_INSTALLATION.md).
|
||||
|
||||
#### 💻 Bare-metal / On-server (Experimental/community supported 🧪)
|
||||
|
||||
- [(Experimental 🧪) On-hardware instructions](./HW_INSTALL.md)
|
||||
- [(Experimental 🧪) On-hardware instructions](./HW_INSTALL.md)
|
||||
|
||||
- Alternative bare-metal install forks:
|
||||
- Alternative bare-metal install forks:
|
||||
- [leiweibau's fork](https://github.com/leiweibau/Pi.Alert/) (maintained)
|
||||
- [pucherot's original code](https://github.com/pucherot/Pi.Alert/) (un-maintained)
|
||||
|
||||
@@ -63,7 +63,6 @@ There is also an in-app Help / FAQ section that should be answering frequently a
|
||||
|
||||
#### ♻ Misc
|
||||
|
||||
- [Version history (legacy)](./VERSIONS_HISTORY.md)
|
||||
- [Reverse proxy (Nginx, Apache, SWAG)](./REVERSE_PROXY.md)
|
||||
- [Installing Updates](./UPDATES.md)
|
||||
- [Setting up Authelia](./AUTHELIA.md) (DRAFT)
|
||||
@@ -80,27 +79,27 @@ There is also an in-app Help / FAQ section that should be answering frequently a
|
||||
- [Frontend development tips](./FRONTEND_DEVELOPMENT.md)
|
||||
- [Webhook secrets](./WEBHOOK_SECRET.md)
|
||||
|
||||
Feel free to suggest or submit new docs via a PR.
|
||||
Feel free to suggest or submit new docs via a PR.
|
||||
|
||||
## 👨💻 Development priorities
|
||||
|
||||
Priorities from highest to lowest:
|
||||
|
||||
* 🔼 Fixing core functionality bugs not solvable with workarounds
|
||||
* 🔵 New core functionality unlocking other opportunities (e.g.: plugins)
|
||||
* 🔵 Refactoring enabling faster implementation of future functionality
|
||||
* 🔵 New core functionality unlocking other opportunities (e.g.: plugins)
|
||||
* 🔵 Refactoring enabling faster implementation of future functionality
|
||||
* 🔽 (low) UI functionality & improvements (PRs welcome 😉)
|
||||
|
||||
Design philosophy: Focus on core functionality and leverage existing apps and tools to make NetAlertX integrate into other workflows.
|
||||
Design philosophy: Focus on core functionality and leverage existing apps and tools to make NetAlertX integrate into other workflows.
|
||||
|
||||
Examples:
|
||||
Examples:
|
||||
|
||||
1. Supporting apprise makes more sense than implementing multiple individual notification gateways
|
||||
2. Implementing regular expression support across settings for validation makes more sense than validating one setting with a specific expression.
|
||||
2. Implementing regular expression support across settings for validation makes more sense than validating one setting with a specific expression.
|
||||
|
||||
UI-specific requests are a low priority as the framework picked by the original developer is not very extensible (and afaik doesn't support components) and has limited mobile support. Also, I argue the value proposition is smaller than working on something else.
|
||||
|
||||
Feel free to submit PRs if interested. try to **keep the PRs small/on-topic** so they are easier to review and approve.
|
||||
Feel free to submit PRs if interested. try to **keep the PRs small/on-topic** so they are easier to review and approve.
|
||||
|
||||
That being said, I'd reconsider if more people and or recurring sponsors file a request 😉.
|
||||
|
||||
@@ -112,8 +111,8 @@ Please be as detailed as possible with **workarounds** you considered and why a
|
||||
|
||||
If you submit a PR please:
|
||||
|
||||
1. Check that your changes are backward compatible with existing installations and with a blank setup.
|
||||
2. Existing features should always be preserved.
|
||||
1. Check that your changes are backward compatible with existing installations and with a blank setup.
|
||||
2. Existing features should always be preserved.
|
||||
3. Keep the PR small, on-topic and don't change code that is not necessary for the PR to work
|
||||
4. New features code should ideally be re-usable for different purposes, not for a very narrow use case.
|
||||
5. New functionality should ideally be implemented via the Plugins system, if possible.
|
||||
@@ -131,13 +130,13 @@ Suggested test cases:
|
||||
Some additional context:
|
||||
|
||||
* Permanent settings/config is stored in the `app.conf` file
|
||||
* Currently temporary (session?) settings are stored in the `Parameters` DB table as key-value pairs. This table is wiped during a container rebuild/restart and its values are re-initialized from cookies/session data from the browser.
|
||||
* Currently temporary (session?) settings are stored in the `Parameters` DB table as key-value pairs. This table is wiped during a container rebuild/restart and its values are re-initialized from cookies/session data from the browser.
|
||||
|
||||
## 🐛 Submitting an issue or bug
|
||||
|
||||
Before submitting a new issue please spend a couple of minutes on research:
|
||||
|
||||
* Check [🛑 Common issues](./DEBUG_TIPS.md#common-issues)
|
||||
* Check [🛑 Common issues](./DEBUG_TIPS.md#common-issues)
|
||||
* Check [💡 Closed issues](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed) if a similar issue was solved in the past.
|
||||
* When submitting an issue ❗[enable debug](./DEBUG_TIPS.md)❗
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ You can configure a custom **/etc/resolv.conf** file in **docker-compose.yml** a
|
||||
#### docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
|
||||
@@ -9,21 +9,23 @@ The folders you are creating below will contain the configuration and the databa
|
||||
1. Create a parent folder named `netalertx`
|
||||
2. Create a `db` sub-folder
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
3. Create a `config` sub-folder
|
||||
|
||||

|
||||

|
||||
|
||||
4. Note down the folders Locations:
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
5. Open **Container manager** -> **Project** and click **Create**.
|
||||
6. Fill in the details:
|
||||
## Creating the Project
|
||||
|
||||
1. Open **Container manager** -> **Project** and click **Create**.
|
||||
2. Fill in the details:
|
||||
|
||||
- Project name: `netalertx`
|
||||
- Path: `/app_storage/netalertx` (will differ from yours)
|
||||
@@ -31,7 +33,6 @@ The folders you are creating below will contain the configuration and the databa
|
||||
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
@@ -47,8 +48,7 @@ services:
|
||||
- NET_ADMIN
|
||||
- NET_BIND_SERVICE
|
||||
volumes:
|
||||
- /app_storage/netalertx/config:/data/config
|
||||
- /app_storage/netalertx/db:/data/db
|
||||
- /app_storage/netalertx:/data
|
||||
# to sync with system time
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
tmpfs:
|
||||
@@ -58,35 +58,65 @@ services:
|
||||
- PORT=20211
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
7. Replace the paths to your volume and comment out unnecessary line(s):
|
||||
3. Replace the paths to your volume and comment out unnecessary line(s).
|
||||
|
||||
- This is only an example, your paths will differ.
|
||||
> This is only an example, your paths will differ.
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /volume1/app_storage/netalertx/config:/data/config
|
||||
- /volume1/app_storage/netalertx/db:/data/db
|
||||
# (optional) useful for debugging if you have issues setting up the container
|
||||
# - local/path/logs:/tmp/log <- commented out with # ⚠
|
||||
volumes:
|
||||
- /volume1/app_storage/netalertx:/data
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
8. (optional) Change the port number from `20211` to an unused port if this port is already used.
|
||||
9. Build the project:
|
||||
4. (optional) Change the port number from `20211` to an unused port if this port is already used.
|
||||
5. Build the project:
|
||||
|
||||

|
||||

|
||||
|
||||
10. Navigate to `<Synology URL>:20211` (or your custom port).
|
||||
11. Read the [Subnets](./SUBNETS.md) and [Plugins](/docs/PLUGINS.md) docs to complete your setup.
|
||||
|
||||
## Solving permission issues
|
||||
|
||||
See also the [Permission overview guide](./FILE_PERMISSIONS.md).
|
||||
|
||||
### Configuring the permissions via SSH
|
||||
|
||||
> [!TIP]
|
||||
> If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the `/local_data_dir/db` and `/local_data_dir/config` folders (replace `local_data_dir` with the location where your `/db` and `/config` folders are located).
|
||||
>
|
||||
> `sudo chown -R 20211:20211 /local_data_dir`
|
||||
>
|
||||
> `sudo chmod -R a+rwx /local_data_dir1`
|
||||
> `sudo chmod -R a+rwx /local_data_dir`
|
||||
>
|
||||
|
||||
### Configuring the permissions via the Synology UI
|
||||
|
||||
You can also execute the above bash commands via the UI by creating a one-off scheduled task.
|
||||
|
||||
1. Control panel -> Task Scheduler
|
||||
2. Create -> Scheduled Task -> User-defined Script
|
||||
|
||||

|
||||
|
||||
3. Give your task a name.
|
||||
|
||||

|
||||
|
||||
4. Specify one-off execution time (e.g. 5 minutes from now).
|
||||
|
||||

|
||||
|
||||
5. Paste the commands from the above SSH section and replace the `/local_data_dir` with the parent fodler of your `/db` and `/config` folders.
|
||||
|
||||

|
||||
|
||||
6. Wait until the execution time passes and verify the new ownership.
|
||||
|
||||

|
||||
|
||||
|
||||
In case of issues, double-check the [Permission overview guide](./FILE_PERMISSIONS.md).
|
||||
|
||||
BIN
docs/img/PIHOLE_GUIDE/PIHOLEAPI_settings.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
BIN
docs/img/SYNOLOGY/10_permissions_before.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/img/SYNOLOGY/11_permissions_create_scheduled_task.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/img/SYNOLOGY/12_permissions_task_general.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/img/SYNOLOGY/13_permissions_task_schedule.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/img/SYNOLOGY/14_permissions_task_settings.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/img/SYNOLOGY/15_permissions_after.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 7.9 KiB |
BIN
docs/img/netalertx_docs_old2.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
@@ -72,7 +72,7 @@ a[target="_blank"] {
|
||||
|
||||
[data-is-valid="0"] {
|
||||
/* border: 1px solid red; */
|
||||
background-color: #ff4b4b;
|
||||
background-color: #ff4b4b !important;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
@@ -1825,10 +1825,21 @@ input[readonly] {
|
||||
#networkTree
|
||||
{
|
||||
margin-left: 16px;
|
||||
/* border: solid;
|
||||
border-color:#606060; */
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#networkTree .node-inner {
|
||||
font-size: clamp(12px, 1rem, 18px);
|
||||
}
|
||||
|
||||
#networkTree .netNodeText strong,
|
||||
#networkTree .spanNetworkTree {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
#networkTree .netIcon
|
||||
{
|
||||
width: 25px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--
|
||||
#---------------------------------------------------------------------------------#
|
||||
# NetAlertX #
|
||||
# Open Source Network Guard / WIFI & LAN intrusion detector #
|
||||
# NetAlertX #
|
||||
# Open Source Network Guard / WIFI & LAN intrusion detector #
|
||||
# #
|
||||
# devices.php - Front module. Devices list page #
|
||||
#---------------------------------------------------------------------------------#
|
||||
@@ -15,7 +15,7 @@
|
||||
<?php
|
||||
|
||||
require 'php/templates/header.php';
|
||||
|
||||
|
||||
// check permissions
|
||||
// Use environment-aware paths with fallback to legacy locations
|
||||
$dbFolderPath = rtrim(getenv('NETALERTX_DB') ?: '/data/db', '/');
|
||||
@@ -36,7 +36,7 @@
|
||||
?>
|
||||
|
||||
<!-- ----------------------------------------------------------------------- -->
|
||||
|
||||
|
||||
|
||||
<!-- Page ------------------------------------------------------------------ -->
|
||||
<div class="content-wrapper">
|
||||
@@ -55,15 +55,15 @@
|
||||
<div class="col-md-12">
|
||||
<div class="box" id="clients">
|
||||
<div class="box-header ">
|
||||
<h3 class="box-title col-md-12"><?= lang('Device_Shortcut_OnlineChart');?> </h3>
|
||||
<h3 class="box-title col-md-12"><?= lang('Device_Shortcut_OnlineChart');?> </h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="chart">
|
||||
<script src="lib/chart.js/Chart.js?v=<?php include 'php/templates/version.php'; ?>"></script>
|
||||
<!-- presence chart -->
|
||||
<?php
|
||||
<?php
|
||||
require 'php/components/graph_online_history.php';
|
||||
?>
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
@@ -74,7 +74,7 @@
|
||||
<!-- Device Filters ------------------------------------------------------- -->
|
||||
<div class="box box-aqua hidden" id="columnFiltersWrap">
|
||||
<div class="box-header ">
|
||||
<h3 class="box-title col-md-12"><?= lang('Devices_Filters');?> </h3>
|
||||
<h3 class="box-title col-md-12"><?= lang('Devices_Filters');?> </h3>
|
||||
</div>
|
||||
<!-- Placeholder ------------------------------------------------------- -->
|
||||
<div id="columnFilters" ></div>
|
||||
@@ -88,8 +88,8 @@
|
||||
<!-- box-header -->
|
||||
<div class="box-header">
|
||||
<div class=" col-sm-8 ">
|
||||
<h3 id="tableDevicesTitle" class="box-title text-gray "></h3>
|
||||
</div>
|
||||
<h3 id="tableDevicesTitle" class="box-title text-gray "></h3>
|
||||
</div>
|
||||
<div class="dummyDevice col-sm-4 ">
|
||||
<span id="multiEditPlc">
|
||||
<!-- multi edit button placeholder -->
|
||||
@@ -104,8 +104,8 @@
|
||||
<div class="box-body table-responsive">
|
||||
<table id="tableDevices" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<tr>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
@@ -122,7 +122,7 @@
|
||||
<!-- ----------------------------------------------------------------------- -->
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
|
||||
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
|
||||
@@ -136,9 +136,9 @@
|
||||
<!-- page script ----------------------------------------------------------- -->
|
||||
<script>
|
||||
var deviceStatus = 'all';
|
||||
var tableRows = getCache ("nax_parTableRows") == "" ? parseInt(getSetting("UI_DEFAULT_PAGE_SIZE")) : getCache ("nax_parTableRows") ;
|
||||
|
||||
var tableOrder = getCache ("nax_parTableOrder") == "" ? [[3,'desc'], [0,'asc']] : JSON.parse(getCache ("nax_parTableOrder")) ;
|
||||
|
||||
|
||||
var tableColumnHide = [];
|
||||
var tableColumnOrder = [];
|
||||
var tableColumnVisible = [];
|
||||
@@ -161,7 +161,7 @@ function main () {
|
||||
|
||||
//initialize the table headers in the correct order
|
||||
var availableColumns = getSettingOptions("UI_device_columns").split(",");
|
||||
headersDefaultOrder = availableColumns.map(val => getString(val));
|
||||
headersDefaultOrder = availableColumns.map(val => getString(val));
|
||||
|
||||
var selectedColumns = JSON.parse(getSetting("UI_device_columns").replace(/'/g, '"'));
|
||||
|
||||
@@ -190,10 +190,10 @@ function main () {
|
||||
|
||||
// Initialize components with parameters
|
||||
initializeDatatable(getUrlAnchor('my_devices'));
|
||||
|
||||
|
||||
// check if data outdated and show spinner if so
|
||||
handleLoadingDialog()
|
||||
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -202,7 +202,7 @@ function mapIndx(oldIndex)
|
||||
{
|
||||
// console.log(oldIndex);
|
||||
// console.log(tableColumnOrder);
|
||||
|
||||
|
||||
for(i=0;i<tableColumnOrder.length;i++)
|
||||
{
|
||||
if(tableColumnOrder[i] == oldIndex)
|
||||
@@ -311,7 +311,7 @@ function processDeviceTotals(devicesData) {
|
||||
}
|
||||
});
|
||||
|
||||
// Render info boxes/tile cards
|
||||
// Render info boxes/tile cards
|
||||
renderInfoboxes(dataArray);
|
||||
}
|
||||
|
||||
@@ -350,9 +350,9 @@ function initFilters() {
|
||||
nocache: Date.now() // Prevent caching with a timestamp
|
||||
},
|
||||
success: function(response) {
|
||||
if (response && response.data) {
|
||||
|
||||
let resultJSON = response.data;
|
||||
if (response && response.data) {
|
||||
|
||||
let resultJSON = response.data;
|
||||
|
||||
// Save the result to cache
|
||||
setCache("devicesFilters", JSON.stringify(resultJSON));
|
||||
@@ -381,7 +381,7 @@ function initFilters() {
|
||||
});
|
||||
|
||||
// Filter resultJSON to include only entries with columnName in columnFilters
|
||||
resultJSON = resultJSON.filter(entry =>
|
||||
resultJSON = resultJSON.filter(entry =>
|
||||
columnFilters.some(filter => filter[0] === entry.columnName)
|
||||
);
|
||||
|
||||
@@ -451,7 +451,7 @@ function initFilters() {
|
||||
function renderFilters(customData) {
|
||||
|
||||
// console.log(JSON.stringify(customData));
|
||||
|
||||
|
||||
// Load filter data from the JSON file
|
||||
$.ajax({
|
||||
url: 'php/components/devices_filters.php', // PHP script URL
|
||||
@@ -471,7 +471,7 @@ function renderFilters(customData) {
|
||||
|
||||
// Update DataTable with the new filters or search value (if applicable)
|
||||
$('#tableDevices').DataTable().draw();
|
||||
|
||||
|
||||
// Optionally, apply column filters (if using filters for individual columns)
|
||||
const table = $('#tableDevices').DataTable();
|
||||
table.columnFilters = columnFilters; // Apply your column filters logic
|
||||
@@ -493,11 +493,11 @@ function collectFilters() {
|
||||
// Loop through each filter group
|
||||
document.querySelectorAll('.filter-group').forEach(filterGroup => {
|
||||
const dropdown = filterGroup.querySelector('.filter-dropdown');
|
||||
|
||||
|
||||
if (dropdown) {
|
||||
const filterColumn = dropdown.getAttribute('data-column');
|
||||
const filterValue = dropdown.value;
|
||||
|
||||
|
||||
if (filterValue && filterColumn) {
|
||||
columnFilters.push({
|
||||
filterColumn: filterColumn,
|
||||
@@ -548,7 +548,7 @@ function mapColumnIndexToFieldName(index, tableColumnVisible) {
|
||||
"devReqNicsOnline" // 29
|
||||
];
|
||||
|
||||
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
|
||||
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
|
||||
|
||||
return columnNames[tableColumnOrder[index]] || null;
|
||||
}
|
||||
@@ -557,12 +557,15 @@ function mapColumnIndexToFieldName(index, tableColumnVisible) {
|
||||
// ---------------------------------------------------------
|
||||
// Initializes the main devices list datatable
|
||||
function initializeDatatable (status) {
|
||||
|
||||
|
||||
if(!status)
|
||||
{
|
||||
status = 'my_devices'
|
||||
}
|
||||
|
||||
// retrieve page size
|
||||
var tableRows = getCache ("nax_parTableRows") == "" ? parseInt(getSetting("UI_DEFAULT_PAGE_SIZE")) : getCache ("nax_parTableRows") ;
|
||||
|
||||
// Save status selected
|
||||
deviceStatus = status;
|
||||
|
||||
@@ -579,7 +582,7 @@ function initializeDatatable (status) {
|
||||
case 'all_devices': tableTitle = getString('Gen_All_Devices'); color = 'gray'; break;
|
||||
case 'network_devices': tableTitle = getString('Network_Devices'); color = 'aqua'; break;
|
||||
default: tableTitle = getString('Device_Shortcut_Devices'); color = 'gray'; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set title and color
|
||||
$('#tableDevicesTitle')[0].className = 'box-title text-'+ color;
|
||||
@@ -588,23 +591,23 @@ function initializeDatatable (status) {
|
||||
|
||||
// render table headers
|
||||
html = '';
|
||||
|
||||
|
||||
for(index = 0; index < tableColumnOrder.length; index++)
|
||||
{
|
||||
html += '<th>' + headersDefaultOrder[tableColumnOrder[index]] + '</th>';
|
||||
}
|
||||
|
||||
$('#tableDevices tr').html(html);
|
||||
$('#tableDevices tr').html(html);
|
||||
|
||||
hideUIelements("UI_DEV_SECTIONS")
|
||||
|
||||
for(i = 0; i < tableColumnOrder.length; i++)
|
||||
{
|
||||
// hide this column if not in the tableColumnVisible variable (we need to keep the MAC address (index 11) for functionality reasons)
|
||||
{
|
||||
// hide this column if not in the tableColumnVisible variable (we need to keep the MAC address (index 11) for functionality reasons)
|
||||
if(tableColumnVisible.includes(tableColumnOrder[i]) == false)
|
||||
{
|
||||
tableColumnHide.push(mapIndx(tableColumnOrder[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var table = $('#tableDevices').DataTable({
|
||||
@@ -690,7 +693,7 @@ function initializeDatatable (status) {
|
||||
"status": deviceStatus,
|
||||
"filters" : columnFilters
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -766,8 +769,8 @@ function initializeDatatable (status) {
|
||||
|
||||
// Parameters
|
||||
'pageLength' : tableRows,
|
||||
'order' : tableOrder,
|
||||
'select' : true, // Enable selection
|
||||
'order' : tableOrder,
|
||||
'select' : true, // Enable selection
|
||||
|
||||
'fixedHeader': true,
|
||||
'fixedHeader': {
|
||||
@@ -776,19 +779,19 @@ function initializeDatatable (status) {
|
||||
},
|
||||
|
||||
'columnDefs' : [
|
||||
{visible: false, targets: tableColumnHide },
|
||||
{className: 'text-center', targets: [mapIndx(4), mapIndx(9), mapIndx(10), mapIndx(15), mapIndx(18)] },
|
||||
{className: 'iconColumn text-center', targets: [mapIndx(3)]},
|
||||
{width: '80px', targets: [mapIndx(6), mapIndx(7), mapIndx(15), mapIndx(27)] },
|
||||
{width: '85px', targets: [mapIndx(9)] },
|
||||
{width: '30px', targets: [mapIndx(3), mapIndx(10), mapIndx(13), mapIndx(18)] },
|
||||
{visible: false, targets: tableColumnHide },
|
||||
{className: 'text-center', targets: [mapIndx(4), mapIndx(9), mapIndx(10), mapIndx(15), mapIndx(18)] },
|
||||
{className: 'iconColumn text-center', targets: [mapIndx(3)]},
|
||||
{width: '80px', targets: [mapIndx(6), mapIndx(7), mapIndx(15), mapIndx(27)] },
|
||||
{width: '85px', targets: [mapIndx(9)] },
|
||||
{width: '30px', targets: [mapIndx(3), mapIndx(10), mapIndx(13), mapIndx(18)] },
|
||||
{orderData: [mapIndx(12)], targets: mapIndx(8) },
|
||||
|
||||
// Device Name and FQDN
|
||||
{targets: [mapIndx(0), mapIndx(27)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
|
||||
// console.log(cellData)
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
|
||||
// console.log(cellData)
|
||||
$(td).html (
|
||||
`<b class="anonymizeDev "
|
||||
>
|
||||
@@ -811,9 +814,9 @@ function initializeDatatable (status) {
|
||||
);
|
||||
} },
|
||||
|
||||
// Connected Devices
|
||||
// Connected Devices
|
||||
{targets: [mapIndx(15)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
// check if this is a network device
|
||||
if(getSetting("NETWORK_DEVICE_TYPES").includes(`'${rowData[mapIndx(2)]}'`) )
|
||||
{
|
||||
@@ -823,13 +826,13 @@ function initializeDatatable (status) {
|
||||
{
|
||||
$(td).html (`<i class="fa-solid fa-xmark" title="${getString("Device_Table_Not_Network_Device")}"></i>`)
|
||||
}
|
||||
|
||||
|
||||
} },
|
||||
|
||||
// Icon
|
||||
// Icon
|
||||
{targets: [mapIndx(3)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
|
||||
|
||||
if (!emptyArr.includes(cellData)){
|
||||
$(td).html (atob(cellData));
|
||||
} else {
|
||||
@@ -837,7 +840,7 @@ function initializeDatatable (status) {
|
||||
}
|
||||
} },
|
||||
|
||||
// Full MAC
|
||||
// Full MAC
|
||||
{targets: [mapIndx(11)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
if (!emptyArr.includes(cellData)){
|
||||
@@ -846,8 +849,8 @@ function initializeDatatable (status) {
|
||||
$(td).html ('');
|
||||
}
|
||||
} },
|
||||
|
||||
// IP address
|
||||
|
||||
// IP address
|
||||
{targets: [mapIndx(8)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
if (!emptyArr.includes(cellData)){
|
||||
@@ -864,9 +867,9 @@ function initializeDatatable (status) {
|
||||
} else {
|
||||
$(td).html ('');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// IP address (ordeable)
|
||||
// IP address (ordeable)
|
||||
{targets: [mapIndx(12)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
if (!emptyArr.includes(cellData)){
|
||||
@@ -874,10 +877,10 @@ function initializeDatatable (status) {
|
||||
} else {
|
||||
$(td).html ('');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Custom Properties
|
||||
// Custom Properties
|
||||
{targets: [mapIndx(26)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
if (!emptyArr.includes(cellData)){
|
||||
@@ -885,10 +888,10 @@ function initializeDatatable (status) {
|
||||
} else {
|
||||
$(td).html ('');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Favorite
|
||||
|
||||
// Favorite
|
||||
{targets: [mapIndx(4)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
if (cellData == 1){
|
||||
@@ -897,8 +900,8 @@ function initializeDatatable (status) {
|
||||
$(td).html ('');
|
||||
}
|
||||
} },
|
||||
|
||||
// Dates
|
||||
|
||||
// Dates
|
||||
{targets: [mapIndx(6), mapIndx(7)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
var result = cellData.toString(); // Convert to string
|
||||
@@ -908,7 +911,7 @@ function initializeDatatable (status) {
|
||||
$(td).html (translateHTMLcodes (result));
|
||||
} },
|
||||
|
||||
// Random MAC
|
||||
// Random MAC
|
||||
{targets: [mapIndx(9)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
// console.log(cellData)
|
||||
@@ -919,7 +922,7 @@ function initializeDatatable (status) {
|
||||
}
|
||||
} },
|
||||
|
||||
// Parent Mac
|
||||
// Parent Mac
|
||||
{targets: [mapIndx(14)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
if (!isValidMac(cellData)) {
|
||||
@@ -938,13 +941,13 @@ function initializeDatatable (status) {
|
||||
|
||||
const chipHtml = renderDeviceLink(data, spanWrap, true); // pass the td as container
|
||||
|
||||
$(spanWrap).append(chipHtml);
|
||||
}
|
||||
$(spanWrap).append(chipHtml);
|
||||
}
|
||||
},
|
||||
// Status color
|
||||
// Status color
|
||||
{targets: [mapIndx(10)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
|
||||
|
||||
tmp_devPresentLastScan = rowData[mapIndx(24)]
|
||||
tmp_devAlertDown = rowData[mapIndx(25)]
|
||||
|
||||
@@ -954,11 +957,11 @@ function initializeDatatable (status) {
|
||||
rowData[mapIndx(11)], // MAC
|
||||
cellData // optional text
|
||||
);
|
||||
|
||||
|
||||
$(td).html (`<a href="${badge.url}" class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.text}</a>`);
|
||||
} },
|
||||
],
|
||||
|
||||
|
||||
// Processing
|
||||
'processing' : true,
|
||||
'language' : {
|
||||
@@ -978,7 +981,7 @@ function initializeDatatable (status) {
|
||||
$('#tableDevices').on( 'length.dt', function ( e, settings, len ) {
|
||||
setCache ("nax_parTableRows", len, 129600); // save for 90 days
|
||||
} );
|
||||
|
||||
|
||||
$('#tableDevices').on( 'order.dt', function () {
|
||||
setCache ("nax_parTableOrder", JSON.stringify (table.order()), 129600); // save for 90 days
|
||||
} );
|
||||
@@ -998,7 +1001,7 @@ function initializeDatatable (status) {
|
||||
// Toggle visibility of element with ID 'multiEdit'
|
||||
$('#multiEdit').toggle(anyRowSelected);
|
||||
}, 100);
|
||||
|
||||
|
||||
});
|
||||
|
||||
// search only after idle
|
||||
@@ -1014,59 +1017,59 @@ function initializeDatatable (status) {
|
||||
}, debounceTime);
|
||||
});
|
||||
|
||||
initHoverNodeInfo();
|
||||
initHoverNodeInfo();
|
||||
hideSpinner();
|
||||
|
||||
|
||||
},
|
||||
createdRow: function(row, data, dataIndex) {
|
||||
// add devMac to the table row
|
||||
$(row).attr('my-devMac', data[mapIndx(11)]);
|
||||
|
||||
$(row).attr('my-devMac', data[mapIndx(11)]);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function handleLoadingDialog(needsReload = false)
|
||||
{
|
||||
// console.log(`needsReload: ${needsReload}`);
|
||||
// console.log(`needsReload: ${needsReload}`);
|
||||
|
||||
$.get('php/server/query_logs.php?file=execution_queue.log&nocache=' + Date.now(), function(data) {
|
||||
$.get('php/server/query_logs.php?file=execution_queue.log&nocache=' + Date.now(), function(data) {
|
||||
|
||||
if(data.includes("update_api|devices"))
|
||||
{
|
||||
{
|
||||
showSpinner("devices_old")
|
||||
|
||||
setTimeout(handleLoadingDialog(true), 1000);
|
||||
|
||||
} else if (needsReload)
|
||||
{
|
||||
location.reload();
|
||||
{
|
||||
location.reload();
|
||||
}else
|
||||
{
|
||||
// hideSpinner();
|
||||
}
|
||||
// hideSpinner();
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Function collects selected devices in the DataTable and redirects the user to
|
||||
// the Miantenance section with a 'macs' query string identifying selected devices
|
||||
// Function collects selected devices in the DataTable and redirects the user to
|
||||
// the Miantenance section with a 'macs' query string identifying selected devices
|
||||
function multiEditDevices()
|
||||
{
|
||||
// get selected devices
|
||||
var selectedDevicesDataTableData = $('#tableDevices').DataTable().rows({ selected: true, page: 'current' }).data().toArray();
|
||||
|
||||
console.log(selectedDevicesDataTableData);
|
||||
|
||||
|
||||
macs = ""
|
||||
|
||||
for (var j = 0; j < selectedDevicesDataTableData.length; j++) {
|
||||
macs += selectedDevicesDataTableData[j][mapIndx(11)] + ","; // [11] == MAC
|
||||
macs += selectedDevicesDataTableData[j][mapIndx(11)] + ","; // [11] == MAC
|
||||
}
|
||||
|
||||
// redirect to the Maintenance section
|
||||
@@ -1075,7 +1078,7 @@ function multiEditDevices()
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Function collects shown devices from the DataTable
|
||||
// Function collects shown devices from the DataTable
|
||||
function getMacsOfShownDevices() {
|
||||
var table = $('#tableDevices').DataTable();
|
||||
|
||||
@@ -1096,15 +1099,15 @@ function getMacsOfShownDevices() {
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Handle custom actions/properties on a device
|
||||
// Handle custom actions/properties on a device
|
||||
function renderCustomProps(custProps, mac) {
|
||||
// Decode and parse the custom properties
|
||||
|
||||
if (!isBase64(custProps)) {
|
||||
|
||||
console.error(`Unable to decode CustomProps for ${mac}`);
|
||||
console.error(custProps);
|
||||
|
||||
console.error(`Unable to decode CustomProps for ${mac}`);
|
||||
console.error(custProps);
|
||||
|
||||
} else{
|
||||
const props = JSON.parse(atob(custProps));
|
||||
let html = "";
|
||||
@@ -1150,13 +1153,13 @@ function renderCustomProps(custProps, mac) {
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Update cache with shown devices before navigating away
|
||||
// Update cache with shown devices before navigating away
|
||||
window.addEventListener('beforeunload', function(event) {
|
||||
// Call your function here
|
||||
macs = getMacsOfShownDevices();
|
||||
|
||||
setCache("ntx_visible_macs", macs)
|
||||
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB |
452
front/img/svg/netalertx_docs_blue.svg
Normal file
@@ -0,0 +1,452 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 52.916667 52.916668"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.2 (b8e25be833, 2022-02-05)"
|
||||
sodipodi:docname="netalertx_red_docs_copy3_blue.svg"
|
||||
inkscape:export-filename="C:\Users\jokob\netalertx_red_docs_d_1.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.8284271"
|
||||
inkscape:cx="132.40575"
|
||||
inkscape:cy="118.44039"
|
||||
inkscape:window-width="3377"
|
||||
inkscape:window-height="1417"
|
||||
inkscape:window-x="55"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g48055"
|
||||
units="px"
|
||||
width="50px" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
effect="powermask"
|
||||
id="path-effect51283"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
uri="#mask-powermask-path-effect51283"
|
||||
invert="false"
|
||||
hide_mask="false"
|
||||
background="true"
|
||||
background_color="#ffffffff" />
|
||||
<inkscape:path-effect
|
||||
effect="powermask"
|
||||
id="path-effect51278"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
uri="#mask-powermask-path-effect51278"
|
||||
invert="false"
|
||||
hide_mask="false"
|
||||
background="true"
|
||||
background_color="#ffffffff" />
|
||||
<inkscape:path-effect
|
||||
effect="powermask"
|
||||
id="path-effect51273"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
uri="#mask-powermask-path-effect51273"
|
||||
invert="false"
|
||||
hide_mask="false"
|
||||
background="true"
|
||||
background_color="#ffffffff" />
|
||||
<inkscape:path-effect
|
||||
effect="powermask"
|
||||
id="path-effect48754"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
uri="#mask-powermask-path-effect48754"
|
||||
invert="false"
|
||||
hide_mask="false"
|
||||
background="true"
|
||||
background_color="#ffffffff" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath48972">
|
||||
<path
|
||||
style="fill:#000000;stroke-width:0.280643"
|
||||
id="path48974"
|
||||
width="56.128242"
|
||||
height="56.128246"
|
||||
x="-18.924671"
|
||||
y="-56.198174"
|
||||
transform="rotate(45.438374)"
|
||||
mask="none"
|
||||
sodipodi:type="rect" />
|
||||
</clipPath>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask49405">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:60.8695px;line-height:1.25;font-family:Amiri;-inkscape-font-specification:Amiri;display:inline;stroke-width:1.52174"
|
||||
x="66.930733"
|
||||
y="78.642288"
|
||||
id="text49409"
|
||||
transform="scale(1.4861626,0.67287388)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan49407"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Tw Cen MT';-inkscape-font-specification:'Tw Cen MT';fill:#ffffff;stroke-width:1.52174"
|
||||
x="66.930733"
|
||||
y="78.642288">A</tspan></text>
|
||||
</mask>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath50306">
|
||||
<circle
|
||||
style="mix-blend-mode:normal;fill:#d40000;stroke-width:0.176318"
|
||||
id="circle50308"
|
||||
cy="26.458334"
|
||||
cx="26.458334"
|
||||
r="26.458334"
|
||||
clip-path="url(#clipPath48972)"
|
||||
transform="matrix(1.0038771,0,0.00391255,1.0073928,-0.04603368,-0.1228191)" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath48972-7">
|
||||
<path
|
||||
style="fill:#000000;stroke-width:0.280643"
|
||||
id="path48974-5"
|
||||
width="56.128242"
|
||||
height="56.128246"
|
||||
x="-18.924671"
|
||||
y="-56.198174"
|
||||
transform="rotate(45.438374)"
|
||||
mask="none"
|
||||
sodipodi:type="rect" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath50306-6">
|
||||
<circle
|
||||
style="mix-blend-mode:normal;fill:#d40000;stroke-width:0.176318"
|
||||
id="circle50308-5"
|
||||
cy="26.458334"
|
||||
cx="26.458334"
|
||||
r="26.458334"
|
||||
clip-path="url(#clipPath48972)"
|
||||
transform="matrix(1.0038771,0,0.00391255,1.0073928,-0.04603368,-0.1228191)" />
|
||||
</clipPath>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask-powermask-path-effect51273">
|
||||
<path
|
||||
id="mask-powermask-path-effect51273_box"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
d="m 71.788348,33.677177 h 2.00083 v 2.173766 h -2.00083 z" />
|
||||
<path
|
||||
style="fill:#000000"
|
||||
id="path51263"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="66.211845"
|
||||
sodipodi:cy="37.490814"
|
||||
sodipodi:rx="3.9464016"
|
||||
sodipodi:ry="1.4616301"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="0.031086059"
|
||||
sodipodi:open="true"
|
||||
sodipodi:arc-type="arc"
|
||||
d="m 70.158247,37.490814 a 3.9464016,1.4616301 0 0 1 -0.0019,0.04543" />
|
||||
</mask>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask-powermask-path-effect51278">
|
||||
<path
|
||||
style="fill:#000000"
|
||||
id="path51267"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="66.211845"
|
||||
sodipodi:cy="37.490814"
|
||||
sodipodi:rx="3.9464016"
|
||||
sodipodi:ry="1.4616301"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="0.031086059"
|
||||
sodipodi:open="true"
|
||||
sodipodi:arc-type="arc" />
|
||||
</mask>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask-powermask-path-effect51283">
|
||||
<path
|
||||
style="fill:#000000"
|
||||
id="path51271"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="66.211845"
|
||||
sodipodi:cy="37.490814"
|
||||
sodipodi:rx="3.9464016"
|
||||
sodipodi:ry="1.4616301"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="0.031086059"
|
||||
sodipodi:open="true"
|
||||
sodipodi:arc-type="arc" />
|
||||
</mask>
|
||||
<filter
|
||||
id="mask-powermask-path-effect51273_inverse"
|
||||
inkscape:label="filtermask-powermask-path-effect51273"
|
||||
style="color-interpolation-filters:sRGB"
|
||||
height="100"
|
||||
width="100"
|
||||
x="-50"
|
||||
y="-50">
|
||||
<feColorMatrix
|
||||
id="mask-powermask-path-effect51273_primitive1"
|
||||
values="1"
|
||||
type="saturate"
|
||||
result="fbSourceGraphic" />
|
||||
<feColorMatrix
|
||||
id="mask-powermask-path-effect51273_primitive2"
|
||||
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 "
|
||||
in="fbSourceGraphic" />
|
||||
</filter>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath1481">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.227484"
|
||||
id="rect1483"
|
||||
width="26.653997"
|
||||
height="52.852543"
|
||||
x="62.86179"
|
||||
y="-0.46772188" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath1481-1">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.227484"
|
||||
id="rect1483-0"
|
||||
width="26.653997"
|
||||
height="52.852543"
|
||||
x="62.86179"
|
||||
y="-0.46772188" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Red 1"
|
||||
style="display:none">
|
||||
<circle
|
||||
style="fill:#ff2a2a;stroke-width:0.176318"
|
||||
id="path31-8"
|
||||
cy="26.458334"
|
||||
cx="26.458334"
|
||||
r="26.458334" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="A - Layer 2"
|
||||
style="display:none">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.328992"
|
||||
id="rect48998"
|
||||
width="26.0966"
|
||||
height="6.0620313"
|
||||
x="13.255443"
|
||||
y="41.262722" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="g48055"
|
||||
inkscape:label="Red top"
|
||||
style="display:none;mix-blend-mode:normal">
|
||||
<circle
|
||||
style="mix-blend-mode:normal;fill:#d40000;stroke-width:0.176318"
|
||||
id="circle48752"
|
||||
cy="26.458334"
|
||||
cx="26.458334"
|
||||
r="26.458334"
|
||||
clip-path="url(#clipPath48972)"
|
||||
transform="matrix(1.0038771,0,0.00391255,1.0073928,-0.04603368,-0.1228191)" />
|
||||
<ellipse
|
||||
style="display:inline;mix-blend-mode:normal;fill:#000000;stroke-width:0.43638"
|
||||
id="path50080"
|
||||
clip-path="url(#clipPath50306)"
|
||||
ry="13.739323"
|
||||
rx="16.735666"
|
||||
cy="22.874514"
|
||||
cx="26.36149"
|
||||
transform="translate(0,0.09980904)" />
|
||||
<path
|
||||
style="fill:#000000"
|
||||
id="path51325"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="16.772207"
|
||||
sodipodi:cy="26.090099"
|
||||
sodipodi:rx="4.1291056"
|
||||
sodipodi:ry="7.6004772"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="0.031086059"
|
||||
sodipodi:arc-type="slice"
|
||||
d="m 20.901313,26.090099 a 4.1291056,7.6004772 0 0 1 -0.002,0.236231 l -4.127111,-0.236231 z" />
|
||||
<path
|
||||
style="fill:#d40000"
|
||||
id="path51717"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="26.441042"
|
||||
sodipodi:cy="-26.531424"
|
||||
sodipodi:rx="10.418671"
|
||||
sodipodi:ry="9.5820541"
|
||||
sodipodi:start="0.82219863"
|
||||
sodipodi:end="2.3054129"
|
||||
sodipodi:arc-type="slice"
|
||||
d="m 33.532115,-19.511189 a 10.418671,9.5820541 0 0 1 -14.074736,0.09049 l 6.983663,-7.110726 z"
|
||||
transform="matrix(1,0,0.0048047,-0.99998846,0,0)" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.276214"
|
||||
d="M 145.28835,50.354872 C 127.01317,34.62734 98.057144,30.012421 73.710372,38.947003 c -6.518003,2.391924 -14.288822,6.834002 -19.265958,11.01311 -1.198654,1.006465 -2.270358,1.829935 -2.381565,1.829935 -0.111206,0 -5.210052,-5.102002 -11.33077,-11.337781 L 29.603503,29.114489 30.822139,27.851613 c 0.670251,-0.69458 2.51592,-2.384634 4.101489,-3.755674 C 50.725112,10.43241 69.462577,2.3767456 90.736164,0.10085492 95.380582,-0.39601422 106.33043,-0.31105699 111.03786,0.25837091 133.04363,2.9202648 151.46536,11.26468 167.83762,25.986722 l 3.30701,2.97369 -2.29392,2.320103 c -1.26165,1.276057 -6.58213,6.517685 -11.82329,11.648065 l -9.52936,9.327957 z"
|
||||
id="path52311"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.276214"
|
||||
d="M 86.538548,86.634546 74.145111,73.25799 74.899337,72.758689 c 4.93766,-3.268754 10.138703,-6.508578 16.602198,-7.437693 5.484021,-0.788317 12.228205,-0.984814 16.377135,-0.09119 6.77689,1.459652 11.87156,4.340971 17.02452,7.792011 l 0.97468,0.652765 -1.37124,1.269268 c -0.86863,0.804036 -6.82647,6.676301 -13.34742,13.259175 L 99.423152,99.796276 Z"
|
||||
id="path52350"
|
||||
transform="scale(0.26458333)"
|
||||
inkscape:export-filename="C:\Users\jokob\path52350.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
sodipodi:nodetypes="ccsssscsscc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Black"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline">
|
||||
<ellipse
|
||||
style="fill:#000000;stroke-width:0.176146"
|
||||
id="path31"
|
||||
cy="26.51001"
|
||||
cx="26.458334"
|
||||
rx="26.458"
|
||||
ry="26.406658" />
|
||||
<circle
|
||||
style="display:inline;fill:#ffffff;stroke-width:0.176318"
|
||||
id="path31-89"
|
||||
mask="url(#mask49405)"
|
||||
transform="translate(-99.990036,0.02979629)"
|
||||
r="26.458334"
|
||||
cy="26.458334"
|
||||
cx="126.45834" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="M 50.734917,51.5385 C 50.317784,51.008202 45.376222,45.855755 39.753667,40.088624 L 29.530842,29.602927 32.157037,27.108298 C 37.014258,22.494413 44.043654,17.26825 51.002109,13.097503 60.785219,7.2337198 74.185013,2.5922331 86.866814,0.67450934 92.65309,-0.20048258 104.71024,-0.37258331 110.80487,0.33282367 133.37755,2.9454414 150.98136,11.201829 167.87245,27.098183 l 2.76303,2.600302 -11.44673,11.421726 -11.44672,11.421723 -2.63001,-2.20425 C 135.80913,42.540775 123.7472,37.357565 110.13188,35.306142 105.25895,34.571936 94.151456,34.473316 89.625785,35.124073 76.006414,37.082441 65.655848,41.542025 54.928431,50.073566 c -1.679878,1.336011 -3.139997,2.429113 -3.244707,2.429113 -0.104711,0 -0.531674,-0.433881 -0.948807,-0.964179 z"
|
||||
id="path117144"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="m 86.479201,86.655988 -12.859682,-12.863304 1.72756,-1.259375 c 5.937867,-4.328648 15.716974,-7.877579 22.763988,-8.261269 5.344243,-0.290978 12.593953,1.304433 19.011433,4.183761 2.41258,1.08245 8.21218,4.752269 8.21218,5.196429 0,0.224653 -16.50779,16.711429 -23.16256,23.133076 l -2.833236,2.733985 z"
|
||||
id="path117183"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="m 151.14408,181.37289 -2.63396,-2.65165 H 99.719219 50.928317 l -2.558625,2.54155 c -2.367982,2.35218 -2.618861,2.50924 -3.367071,2.10794 -1.632484,-0.87558 -7.984339,-5.82527 -11.691442,-9.11058 l -3.811927,-3.3782 34.882231,-35.14801 c 19.185224,-19.3314 34.980859,-35.144 35.101403,-35.13912 0.120544,0.005 16.129074,15.83285 35.574514,35.17326 l 35.35534,35.16438 -2.12132,1.95782 c -4.15184,3.83183 -13.51513,11.13426 -14.27653,11.13426 -0.13027,0 -1.42214,-1.19324 -2.87081,-2.65165 z M 112.69455,143.27811 99.52528,130.10884 86.35601,143.27811 73.18674,156.44738 h 26.33854 26.33854 z"
|
||||
id="path117222"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="m 43.323744,182.02493 c -3.122315,-2.21745 -8.886633,-6.91043 -11.466851,-9.33566 l -2.129855,-2.00191 31.417516,-31.60357 c 17.279634,-17.38196 32.982312,-33.19165 34.894842,-35.13266 l 3.477325,-3.5291 35.271229,35.28278 35.27123,35.28278 -2.29809,2.12417 c -3.23874,2.99361 -8.21439,6.9674 -11.21429,8.95625 l -2.55224,1.69205 -2.04396,-1.77268 c -1.12418,-0.97498 -2.34704,-2.10872 -2.71748,-2.51941 l -0.67351,-0.74673 H 99.52196 50.484308 l -2.199537,2.47487 c -1.209746,1.36118 -2.306828,2.46959 -2.437961,2.46312 -0.131132,-0.006 -1.266512,-0.7419 -2.523066,-1.6343 z m 82.364486,-25.84451 c 0,-0.14683 -5.88666,-6.15201 -13.08147,-13.34485 L 99.52528,129.75768 86.443805,142.83557 c -7.194812,7.19284 -13.081476,13.19802 -13.081476,13.34485 0,0.14683 11.773328,0.26696 26.162951,0.26696 14.38962,0 26.16295,-0.12013 26.16295,-0.26696 z"
|
||||
id="path117261"
|
||||
transform="scale(0.26458333)" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer6"
|
||||
inkscape:label="Circle"
|
||||
style="display:none">
|
||||
<path
|
||||
style="fill:#000000"
|
||||
id="path50026"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="71.071762"
|
||||
sodipodi:cy="34.677177"
|
||||
sodipodi:rx="1.7174155"
|
||||
sodipodi:ry="5.5907354"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="0.031086059"
|
||||
sodipodi:open="true"
|
||||
sodipodi:arc-type="arc"
|
||||
mask="url(#mask-powermask-path-effect51273)"
|
||||
d="m 72.789178,34.677177 a 1.7174155,5.5907354 0 0 1 -8.3e-4,0.173766"
|
||||
inkscape:path-effect="#path-effect51273" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.276214"
|
||||
d="m 151.08883,181.46994 -2.76213,-2.60427 -48.802077,-0.009 -48.802075,-0.009 -2.292573,2.48592 c -1.260915,1.36726 -2.431589,2.48592 -2.601499,2.48592 -0.869396,0 -9.118995,-6.36599 -13.713669,-10.58246 l -2.688104,-2.46684 34.973647,-35.11455 c 19.235503,-19.313 34.922993,-35.39075 35.029879,-35.39075 0.106889,0 16.231201,16.10588 35.663001,35.45326 l 35.33055,35.17705 -2.48592,2.35505 c -3.08951,2.92687 -7.41515,6.40509 -11.09719,8.92319 -1.54594,1.05725 -2.85105,1.91728 -2.90024,1.9112 -0.0492,-0.006 -1.33242,-1.183 -2.8516,-2.61535 z m -38.4631,-38.32188 -13.050732,-13.05073 -13.050727,13.05073 -13.050725,13.05072 h 26.101452 26.101452 z"
|
||||
id="path52389"
|
||||
transform="scale(0.26458333)"
|
||||
inkscape:export-filename="C:\Users\jokob\path52389.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
sodipodi:nodetypes="ccccssscssscsscccccccccc" />
|
||||
<path
|
||||
style="fill:#d40000;stroke-width:0.276214"
|
||||
d="M 86.416478,86.793237 C 73.427951,73.815968 73.387119,73.801376 73.387119,73.801376 c 3.874197,-3.341721 11.025508,-6.981646 17.312424,-8.529335 2.339787,-0.576001 4.881362,-1.25628 8.810591,-1.259564 4.438736,-0.0037 8.292516,0.857843 13.253396,2.535104 4.59135,1.552325 7.8315,3.224336 11.49958,5.934101 l 1.61476,1.192897 -2.31005,2.336325 c -1.27053,1.284978 -7.22284,7.16236 -13.22736,13.060849 L 99.423152,99.796276 C 95.128284,95.409033 87.282899,87.658907 86.416478,86.793237 Z"
|
||||
id="path52465"
|
||||
transform="scale(0.26458333)"
|
||||
sodipodi:nodetypes="sssssscsscs" />
|
||||
<path
|
||||
style="fill:#d40000;stroke-width:0.074168"
|
||||
d="M 38.412677,13.39572 C 34.322163,9.945267 28.437517,8.4874766 22.684204,9.4993379 19.419721,10.073478 16.752307,11.410793 13.835187,13.872492 l -0.14691,0.126732 -0.587936,-0.661605 c -0.268568,-0.30222 -1.619514,-1.65761 -2.963235,-3.048642 L 7.7265561,7.8632145 7.9975963,7.5868118 C 9.8344314,5.713635 13.005888,3.476019 15.380049,2.3878744 20.659765,-0.03196726 26.24205,-0.73479764 31.856076,0.42838695 36.599757,1.4112419 40.746004,3.5106537 44.46876,7.1557672 l 0.709881,0.6950753 -0.663694,0.69037 C 44.080041,8.9935983 42.672626,10.391271 41.3963,11.655819 L 39.075708,13.955 Z"
|
||||
id="path52504"
|
||||
inkscape:export-filename="C:\Users\jokob\path52504.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
sodipodi:nodetypes="ssscsccsssscsscs" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="M 86.655143,86.478376 73.973101,73.792663 75.700647,72.517799 c 3.888483,-2.869556 11.979097,-6.234087 17.887709,-7.438714 6.781224,-1.382532 16.632394,0.1812 23.791374,3.776537 2.53147,1.271345 7.60139,4.47823 7.60139,4.808126 0,0.217537 -18.217,18.402022 -23.34018,23.298518 l -2.303755,2.201823 z"
|
||||
id="path117417"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="M 86.653362,86.476595 74.004328,73.8239 l 1.78137,-1.307646 c 4.058289,-2.979059 11.996346,-6.266814 18.081148,-7.488783 5.742499,-1.153228 13.433334,-0.173122 20.711924,2.639491 2.64803,1.02326 7.63077,3.765523 9.69377,5.334995 l 0.88241,0.67131 -6.36248,6.41376 c -3.49937,3.527567 -9.3162,9.255172 -12.92628,12.728011 l -6.563793,6.314253 z"
|
||||
id="path117456"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="M 40.755089,40.913849 29.891381,29.698485 32.789887,26.931909 C 38.664423,21.324762 48.374309,14.517657 56.038213,10.633695 66.085649,5.5417911 79.271822,1.6347929 90.224457,0.50447904 c 5.29419,-0.54636158 20.003853,-0.24145692 24.614013,0.51020386 16.55879,2.6998184 30.27274,8.3744041 42.56518,17.6127021 3.66685,2.755798 10.38919,8.484428 12.02678,10.248962 l 0.78546,0.846346 -11.22765,11.223531 -11.22764,11.223531 -2.46252,-1.977749 C 130.84681,38.585569 112.25268,33.14502 92.666988,34.792406 78.082451,36.019136 67.49078,40.200159 55.292129,49.545997 c -1.868753,1.431721 -3.459743,2.598649 -3.535534,2.593173 -0.07579,-0.0055 -5.026468,-5.056871 -11.001506,-11.225321 z"
|
||||
id="path117495"
|
||||
transform="scale(0.26458333)" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="half circle"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="M 50.729651,51.530407 C 50.309622,50.995658 45.365237,45.839438 39.74213,40.072138 L 29.518298,29.586142 32.436819,26.865215 C 37.858508,21.810591 46.002106,15.887672 52.91436,11.971698 62.082793,6.7775379 75.058024,2.4602175 86.866814,0.67450934 92.666822,-0.20255914 104.7089,-0.37259245 110.83899,0.33602379 133.4335,2.9478667 150.81881,11.108766 167.8709,27.107589 l 2.76147,2.590896 -11.424,11.400559 -11.42399,11.400559 -2.65118,-2.175966 C 132.57167,40.013706 117.00056,34.697228 99.348504,34.691269 c -17.588857,-0.0059 -30.84176,4.583432 -44.420073,15.382297 -1.679878,1.336011 -3.139997,2.429113 -3.244707,2.429113 -0.104711,0 -0.534044,-0.437522 -0.954073,-0.972272 z"
|
||||
id="path117300"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="m 86.479787,86.656574 -12.860268,-12.86389 1.72756,-1.257012 c 5.92724,-4.312793 15.575223,-7.833372 22.587211,-8.242144 5.50807,-0.321098 12.64715,1.227498 19.18821,4.162273 2.41292,1.082605 8.21218,4.752294 8.21218,5.196553 0,0.223831 -14.54007,14.745171 -22.63164,22.602487 l -3.362985,3.26562 z"
|
||||
id="path117339"
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
style="opacity:0.98;fill:#5f5fd3;fill-opacity:1;stroke-width:3.81317;stroke-linecap:round;stroke-miterlimit:0.4"
|
||||
d="m 43.323744,182.02493 c -3.01377,-2.14036 -8.648648,-6.71423 -11.329522,-9.19625 l -2.145795,-1.98662 2.929747,-3.04309 c 1.611361,-1.6737 17.298698,-17.50163 34.860748,-35.17319 l 31.931002,-32.13009 35.244626,35.24359 35.24463,35.2436 -2.29809,2.12652 c -3.22978,2.98865 -8.20792,6.96547 -11.21429,8.95861 l -2.55224,1.69205 -2.04396,-1.77268 c -1.12418,-0.97498 -2.34704,-2.10872 -2.71748,-2.51941 l -0.67351,-0.74673 H 99.52196 50.484308 l -2.199537,2.47487 c -1.209746,1.36118 -2.306828,2.46959 -2.437961,2.46312 -0.131132,-0.006 -1.266512,-0.7419 -2.523066,-1.6343 z m 82.364486,-25.84451 c 0,-0.14683 -5.88666,-6.15201 -13.08147,-13.34485 L 99.52528,129.75768 86.443805,142.83557 c -7.194812,7.19284 -13.081476,13.19802 -13.081476,13.34485 0,0.14683 11.773328,0.26696 26.162951,0.26696 14.38962,0 26.16295,-0.12013 26.16295,-0.26696 z"
|
||||
id="path117378"
|
||||
transform="scale(0.26458333)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -1,6 +1,6 @@
|
||||
/* -----------------------------------------------------------------------------
|
||||
* NetAlertX
|
||||
* Open Source Network Guard / WIFI & LAN intrusion detector
|
||||
* Open Source Network Guard / WIFI & LAN intrusion detector
|
||||
*
|
||||
* common.js - Front module. Common Javascript functions
|
||||
*-------------------------------------------------------------------------------
|
||||
@@ -35,16 +35,16 @@ function getCache(key, noCookie = false)
|
||||
// }
|
||||
}
|
||||
|
||||
return "";
|
||||
return "";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function setCache(key, data, expirationMinutes='')
|
||||
{
|
||||
localStorage.setItem(key, data);
|
||||
localStorage.setItem(key, data);
|
||||
|
||||
// // create cookie if expiration set to handle refresh of data
|
||||
// if (expirationMinutes != '')
|
||||
// if (expirationMinutes != '')
|
||||
// {
|
||||
// setCookie ('cache_session_expiry', 'OK', 1)
|
||||
// }
|
||||
@@ -57,7 +57,7 @@ function setCookie (cookie, value, expirationMinutes='') {
|
||||
var expires = '';
|
||||
if (typeof expirationMinutes === 'number') {
|
||||
expires = ';expires=' + new Date(Date.now() + expirationMinutes *60*1000).toUTCString();
|
||||
}
|
||||
}
|
||||
|
||||
// Save Cookie
|
||||
document.cookie = cookie + "=" + value + expires;
|
||||
@@ -107,42 +107,42 @@ function deleteAllCookies() {
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Get settings from the .json file generated by the python backend
|
||||
// and cache them, if available, with options
|
||||
// Get settings from the .json file generated by the python backend
|
||||
// and cache them, if available, with options
|
||||
// -----------------------------------------------------------------------------
|
||||
function cacheSettings()
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!getCache('cacheSettings_completed') === true)
|
||||
{
|
||||
$.get('php/server/query_json.php', { file: 'table_settings.json', nocache: Date.now() }, function(resSet) {
|
||||
{
|
||||
$.get('php/server/query_json.php', { file: 'table_settings.json', nocache: Date.now() }, function(resSet) {
|
||||
|
||||
$.get('php/server/query_json.php', { file: 'plugins.json', nocache: Date.now() }, function(resPlug) {
|
||||
|
||||
pluginsData = resPlug["data"];
|
||||
settingsData = resSet["data"];
|
||||
|
||||
settingsData.forEach((set) => {
|
||||
pluginsData = resPlug["data"];
|
||||
settingsData = resSet["data"];
|
||||
|
||||
settingsData.forEach((set) => {
|
||||
|
||||
resolvedOptions = createArray(set.setOptions)
|
||||
resolvedOptionsOld = resolvedOptions
|
||||
setPlugObj = {};
|
||||
options_params = [];
|
||||
resolved = ""
|
||||
|
||||
|
||||
// proceed only if first option item contains something to resolve
|
||||
if( !set.setKey.includes("__metadata") &&
|
||||
resolvedOptions.length != 0 &&
|
||||
if( !set.setKey.includes("__metadata") &&
|
||||
resolvedOptions.length != 0 &&
|
||||
resolvedOptions[0].includes("{value}"))
|
||||
{
|
||||
// get setting definition from the plugin config if available
|
||||
setPlugObj = getPluginSettingObject(pluginsData, set.setKey)
|
||||
|
||||
// check if options contains parameters and resolve
|
||||
// check if options contains parameters and resolve
|
||||
if(setPlugObj != {} && setPlugObj["options_params"])
|
||||
{
|
||||
// get option_params for {value} resolution
|
||||
options_params = setPlugObj["options_params"]
|
||||
options_params = setPlugObj["options_params"]
|
||||
|
||||
if(options_params != [])
|
||||
{
|
||||
@@ -154,19 +154,19 @@ function cacheSettings()
|
||||
{
|
||||
resolvedOptions = `[${resolved}]`
|
||||
} else // one value only
|
||||
{
|
||||
{
|
||||
resolvedOptions = `["${resolved}"]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCache(`nax_set_${set.setKey}`, set.setValue)
|
||||
setCache(`nax_set_opt_${set.setKey}`, resolvedOptions)
|
||||
setCache(`nax_set_${set.setKey}`, set.setValue)
|
||||
setCache(`nax_set_opt_${set.setKey}`, resolvedOptions)
|
||||
});
|
||||
}).then(() => handleSuccess('cacheSettings', resolve())).catch(() => handleFailure('cacheSettings', reject("cacheSettings already completed"))); // handle AJAX synchronization
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ function getSettingOptions (key) {
|
||||
|
||||
// handle initial load to make sure everything is set-up and cached
|
||||
// handleFirstLoad()
|
||||
|
||||
|
||||
result = getCache(`nax_set_opt_${key}`, true);
|
||||
|
||||
if (result == "")
|
||||
@@ -194,7 +194,7 @@ function getSetting (key) {
|
||||
|
||||
// handle initial load to make sure everything is set-up and cached
|
||||
// handleFirstLoad()
|
||||
|
||||
|
||||
result = getCache(`nax_set_${key}`, true);
|
||||
|
||||
if (result == "")
|
||||
@@ -210,7 +210,7 @@ function getSetting (key) {
|
||||
// -----------------------------------------------------------------------------
|
||||
function cacheStrings() {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
|
||||
// Create a promise for each language (include en_us by default as fallback)
|
||||
languagesToLoad = ['en_us']
|
||||
|
||||
@@ -222,11 +222,11 @@ function cacheStrings() {
|
||||
}
|
||||
|
||||
console.log(languagesToLoad);
|
||||
|
||||
|
||||
const languagePromises = languagesToLoad.map((language_code) => {
|
||||
return new Promise((resolveLang, rejectLang) => {
|
||||
// Fetch core strings and translations
|
||||
|
||||
|
||||
$.get(`php/templates/language/${language_code}.json?nocache=${Date.now()}`)
|
||||
.done((res) => {
|
||||
// Iterate over each key-value pair and store the translations
|
||||
@@ -238,7 +238,7 @@ function cacheStrings() {
|
||||
$.get('php/server/query_json.php', { file: 'table_plugins_language_strings.json', nocache: Date.now() })
|
||||
.done((pluginRes) => {
|
||||
const data = pluginRes["data"];
|
||||
|
||||
|
||||
// Store plugin translations
|
||||
data.forEach((langString) => {
|
||||
setCache(`pia_lang_${langString.String_Key}_${langString.Language_Code}`, langString.String_Value);
|
||||
@@ -269,7 +269,7 @@ function cacheStrings() {
|
||||
// Handle failure in any of the language processing
|
||||
handleFailure('cacheStrings', reject);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ function cacheStrings() {
|
||||
function getString(key) {
|
||||
|
||||
function fetchString(key) {
|
||||
|
||||
|
||||
lang_code = getLangCode();
|
||||
|
||||
let result = getCache(`pia_lang_${key}_${lang_code}`, true);
|
||||
@@ -378,7 +378,7 @@ function localizeTimestamp(input) {
|
||||
let tz = getSetting("TIMEZONE") || 'Europe/Berlin';
|
||||
input = String(input || '').trim();
|
||||
|
||||
// ✅ 1. Unix timestamps (10 or 13 digits)
|
||||
// 1. Unix timestamps (10 or 13 digits)
|
||||
if (/^\d+$/.test(input)) {
|
||||
const ms = input.length === 10 ? parseInt(input, 10) * 1000 : parseInt(input, 10);
|
||||
return new Intl.DateTimeFormat('default', {
|
||||
@@ -389,39 +389,59 @@ function localizeTimestamp(input) {
|
||||
}).format(new Date(ms));
|
||||
}
|
||||
|
||||
// ✅ 2. European DD/MM/YYYY
|
||||
let match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?(.*)$/);
|
||||
// 2. European DD/MM/YYYY
|
||||
let match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?$/);
|
||||
if (match) {
|
||||
let [ , d, m, y, t = "00:00:00", tzPart = "" ] = match;
|
||||
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5?t+":00":t}${tzPart}`;
|
||||
return formatSafe(iso, tz);
|
||||
let [, d, m, y, t = "00:00:00", tzPart = ""] = match;
|
||||
const dNum = parseInt(d, 10);
|
||||
const mNum = parseInt(m, 10);
|
||||
|
||||
if (dNum <= 12 && mNum > 12) {
|
||||
} else {
|
||||
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5 ? t + ":00" : t}${tzPart}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 3. US MM/DD/YYYY
|
||||
// 3. US MM/DD/YYYY
|
||||
match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?(.*)$/);
|
||||
if (match) {
|
||||
let [ , m, d, y, t = "00:00:00", tzPart = "" ] = match;
|
||||
let [, m, d, y, t = "00:00:00", tzPart = ""] = match;
|
||||
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5?t+":00":t}${tzPart}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// ✅ 4. ISO-style (with T, Z, offsets)
|
||||
match = input.match(/^(\d{4}-\d{1,2}-\d{1,2})[ T](\d{1,2}:\d{2}(?::\d{2})?)(Z|[+-]\d{2}:?\d{2})?$/);
|
||||
// 4. ISO YYYY-MM-DD with optional Z/+offset
|
||||
match = input.match(/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[ T](\d{1,2}:\d{2}(?::\d{2})?)(Z|[+-]\d{2}:?\d{2})?$/);
|
||||
if (match) {
|
||||
let [ , ymd, time, offset = "" ] = match;
|
||||
// normalize to YYYY-MM-DD
|
||||
let [y, m, d] = ymd.split('-').map(x => x.padStart(2,'0'));
|
||||
let [, y, m, d, time, offset = ""] = match;
|
||||
const iso = `${y}-${m}-${d}T${time.length===5?time+":00":time}${offset}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// ✅ 5. RFC2822 / "25 Aug 2025 13:45:22 +0200"
|
||||
// 5. RFC2822 / "25 Aug 2025 13:45:22 +0200"
|
||||
match = input.match(/^\d{1,2} [A-Za-z]{3,} \d{4}/);
|
||||
if (match) {
|
||||
return formatSafe(input, tz);
|
||||
}
|
||||
|
||||
// ✅ 6. Fallback (whatever Date() can parse)
|
||||
// 6. DD-MM-YYYY with optional time
|
||||
match = input.match(/^(\d{1,2})-(\d{1,2})-(\d{4})(?:[ T](\d{1,2}:\d{2}(?::\d{2})?))?$/);
|
||||
if (match) {
|
||||
let [, d, m, y, time = "00:00:00"] = match;
|
||||
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${time.length===5?time+":00":time}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// 7. Strict YYYY-DD-MM with optional time
|
||||
match = input.match(/^(\d{4})-(0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])(?:[ T](\d{1,2}:\d{2}(?::\d{2})?))?$/);
|
||||
if (match) {
|
||||
let [, y, d, m, time = "00:00:00"] = match;
|
||||
const iso = `${y}-${m}-${d}T${time.length === 5 ? time + ":00" : time}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// 8. Fallback
|
||||
return formatSafe(input, tz);
|
||||
|
||||
function formatSafe(str, tz) {
|
||||
@@ -440,6 +460,7 @@ function localizeTimestamp(input) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
/**
|
||||
* Replaces double quotes within single-quoted strings, then converts all single quotes to double quotes,
|
||||
@@ -509,7 +530,7 @@ function isBase64(value) {
|
||||
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
if (!base64Regex.test(value)) return false;
|
||||
|
||||
|
||||
|
||||
try {
|
||||
const decoded = atob(value);
|
||||
|
||||
@@ -568,7 +589,7 @@ function decodeSpecialChars(str) {
|
||||
function utf8ToBase64(str) {
|
||||
// Convert the string to a Uint8Array using TextEncoder
|
||||
const utf8Bytes = new TextEncoder().encode(str);
|
||||
|
||||
|
||||
// Convert the Uint8Array to a base64-encoded string
|
||||
return btoa(String.fromCharCode(...utf8Bytes));
|
||||
}
|
||||
@@ -597,31 +618,31 @@ function handle_locked_DB(data)
|
||||
{
|
||||
if(data.includes('database is locked'))
|
||||
{
|
||||
// console.log(data)
|
||||
// console.log(data)
|
||||
showSpinner()
|
||||
|
||||
setTimeout(function() {
|
||||
console.warn("Database locked - reload")
|
||||
location.reload();
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function numberArrayFromString(data)
|
||||
{
|
||||
{
|
||||
data = JSON.parse(sanitize(data));
|
||||
return data.replace(/\[|\]/g, '').split(',').map(Number);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
function saveData(functionName, id, value) {
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "php/server/devices.php",
|
||||
data: { action: functionName, id: id, value:value },
|
||||
success: function(data) {
|
||||
|
||||
success: function(data) {
|
||||
|
||||
if(sanitize(data) == 'OK')
|
||||
{
|
||||
showMessage("Saved")
|
||||
@@ -630,7 +651,7 @@ function saveData(functionName, id, value) {
|
||||
} else
|
||||
{
|
||||
showMessage("ERROR")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -670,13 +691,13 @@ function sleep(milliseconds) {
|
||||
} while (currentDate - date < milliseconds);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ---------------------------------------------------------
|
||||
somethingChanged = false;
|
||||
function settingsChanged()
|
||||
{
|
||||
somethingChanged = true;
|
||||
// Enable navigation prompt ... "Are you sure you want to leave..."
|
||||
window.onbeforeunload = function() {
|
||||
window.onbeforeunload = function() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -694,16 +715,16 @@ function getUrlAnchor(defaultValue){
|
||||
selectedTab = defaultValue
|
||||
|
||||
// the #target from the url
|
||||
target = window.location.hash.substr(1)
|
||||
target = window.location.hash.substr(1)
|
||||
|
||||
// get only the part between #...?
|
||||
if(target.includes('?'))
|
||||
{
|
||||
target = target.split('?')[0]
|
||||
}
|
||||
|
||||
|
||||
return target
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -715,7 +736,7 @@ function getQueryString(key){
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
|
||||
tmp = params[key]
|
||||
tmp = params[key]
|
||||
|
||||
if(emptyArr.includes(tmp))
|
||||
{
|
||||
@@ -726,17 +747,17 @@ function getQueryString(key){
|
||||
|
||||
if (fullUrl.includes('?')) {
|
||||
var queryString = fullUrl.split('?')[1];
|
||||
|
||||
|
||||
// Split the query string into individual parameters
|
||||
var paramsArray = queryString.split('&');
|
||||
|
||||
|
||||
// Loop through the parameters array
|
||||
paramsArray.forEach(function(param) {
|
||||
// Split each parameter into key and value
|
||||
var keyValue = param.split('=');
|
||||
var keyTmp = decodeURIComponent(keyValue[0]);
|
||||
var value = decodeURIComponent(keyValue[1] || '');
|
||||
|
||||
|
||||
// Store key-value pair in the queryParams object
|
||||
queryParams[keyTmp] = value;
|
||||
});
|
||||
@@ -750,7 +771,7 @@ function getQueryString(key){
|
||||
result = emptyArr.includes(tmp) ? "" : tmp;
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
function translateHTMLcodes (text) {
|
||||
if (text == null || emptyArr.includes(text)) {
|
||||
@@ -769,14 +790,14 @@ function translateHTMLcodes (text) {
|
||||
// -----------------------------------------------------------------------------
|
||||
function stopTimerRefreshData () {
|
||||
try {
|
||||
clearTimeout (timerRefreshData);
|
||||
clearTimeout (timerRefreshData);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function newTimerRefreshData (refeshFunction, timeToRefresh) {
|
||||
|
||||
|
||||
if(timeToRefresh && (timeToRefresh != 0 || timeToRefresh != ""))
|
||||
{
|
||||
time = parseInt(timeToRefresh)
|
||||
@@ -813,7 +834,7 @@ function openInNewTab (url) {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
// Navigate to URL if the current URL is not in the provided list of URLs
|
||||
function openUrl(urls) {
|
||||
var currentUrl = window.location.href;
|
||||
@@ -844,21 +865,21 @@ function openUrl(urls) {
|
||||
function forceLoadUrl(relativeUrl) {
|
||||
|
||||
window.location.replace(relativeUrl);
|
||||
window.location.reload()
|
||||
|
||||
window.location.reload()
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function navigateToDeviceWithIp (ip) {
|
||||
|
||||
$.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(res) {
|
||||
|
||||
$.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(res) {
|
||||
|
||||
devices = res["data"];
|
||||
|
||||
mac = ""
|
||||
|
||||
|
||||
$.each(devices, function(index, obj) {
|
||||
|
||||
|
||||
if(obj.devLastIP.trim() == ip.trim())
|
||||
{
|
||||
mac = obj.devMac;
|
||||
@@ -866,7 +887,7 @@ function navigateToDeviceWithIp (ip) {
|
||||
window.open('./deviceDetails.php?mac=' + mac , "_blank");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -898,7 +919,7 @@ function getMac(){
|
||||
});
|
||||
|
||||
return params.mac
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// A function used to make the IP address orderable
|
||||
@@ -950,7 +971,7 @@ function isRandomMAC(mac)
|
||||
{
|
||||
isRandom = false;
|
||||
|
||||
isRandom = ["2", "6", "A", "E", "a", "e"].includes(mac[1]);
|
||||
isRandom = ["2", "6", "A", "E", "a", "e"].includes(mac[1]);
|
||||
|
||||
// if detected as random, make sure it doesn't start with a prefix which teh suer doesn't want to mark as random
|
||||
if(isRandom)
|
||||
@@ -959,17 +980,17 @@ function isRandomMAC(mac)
|
||||
|
||||
if(mac.startsWith(prefix))
|
||||
{
|
||||
isRandom = false;
|
||||
}
|
||||
|
||||
isRandom = false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
return isRandom;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ---------------------------------------------------------
|
||||
// Generate an array object from a string representation of an array
|
||||
function createArray(input) {
|
||||
// Is already array, return
|
||||
@@ -980,25 +1001,25 @@ function isRandomMAC(mac)
|
||||
if (input === '[]' || input === '') {
|
||||
return [];
|
||||
}
|
||||
// handle integer
|
||||
// handle integer
|
||||
if (typeof input === 'number') {
|
||||
input = input.toString();
|
||||
}
|
||||
|
||||
|
||||
// Regex pattern for brackets
|
||||
const patternBrackets = /(^\s*\[)|(\]\s*$)/g;
|
||||
const replacement = '';
|
||||
|
||||
|
||||
// Remove brackets
|
||||
const noBrackets = input.replace(patternBrackets, replacement);
|
||||
|
||||
|
||||
const options = [];
|
||||
|
||||
|
||||
// Detect the type of quote used after the opening bracket
|
||||
const firstChar = noBrackets.trim()[0];
|
||||
const isDoubleQuoted = firstChar === '"';
|
||||
const isSingleQuoted = firstChar === "'";
|
||||
|
||||
|
||||
// Create array while handling commas within quoted segments
|
||||
let currentSegment = '';
|
||||
let withinQuotes = false;
|
||||
@@ -1016,7 +1037,7 @@ function isRandomMAC(mac)
|
||||
}
|
||||
// Push the last segment
|
||||
options.push(currentSegment.trim());
|
||||
|
||||
|
||||
// Remove quotes based on detected type
|
||||
options.forEach((item, index) => {
|
||||
let trimmedItem = item.trim();
|
||||
@@ -1028,7 +1049,7 @@ function isRandomMAC(mac)
|
||||
}
|
||||
options[index] = trimmedItem;
|
||||
});
|
||||
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -1037,7 +1058,7 @@ function isRandomMAC(mac)
|
||||
// for the value to be returned
|
||||
function getDevDataByMac(macAddress, dbColumn) {
|
||||
|
||||
const sessionDataKey = 'devicesListAll_JSON';
|
||||
const sessionDataKey = 'devicesListAll_JSON';
|
||||
const devicesCache = getCache(sessionDataKey);
|
||||
|
||||
if (!devicesCache || devicesCache == "") {
|
||||
@@ -1068,11 +1089,11 @@ function getDevDataByMac(macAddress, dbColumn) {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Cache the devices as one JSON
|
||||
function cacheDevices()
|
||||
{
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
$.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(data) {
|
||||
|
||||
$.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(data) {
|
||||
|
||||
// console.log(data)
|
||||
|
||||
devicesListAll_JSON = data["data"]
|
||||
@@ -1093,11 +1114,11 @@ function cacheDevices()
|
||||
|
||||
// console.log(getCache('devicesListAll_JSON'))
|
||||
}).then(() => handleSuccess('cacheDevices', resolve())).catch(() => handleFailure('cacheDevices', reject("cacheDevices already completed"))); // handle AJAX synchronization
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var devicesListAll_JSON = []; // this will contain a list off all devices
|
||||
var devicesListAll_JSON = []; // this will contain a list off all devices
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function isEmpty(value)
|
||||
@@ -1127,7 +1148,7 @@ function getGuid() {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// UI
|
||||
// UI
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1230,7 +1251,7 @@ function hideSpinner() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Calls a backend function to add a front-end event to an execution queue
|
||||
function updateApi(apiEndpoints)
|
||||
@@ -1250,9 +1271,9 @@ function updateApi(apiEndpoints)
|
||||
})
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
// handling smooth scrolling
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
function setupSmoothScrolling() {
|
||||
// Function to scroll to the element
|
||||
function scrollToElement(id) {
|
||||
@@ -1310,17 +1331,17 @@ function getPluginSettingObject(pluginsData, setting_key, unique_prefix ) {
|
||||
|
||||
result = {}
|
||||
unique_prefix == undefined ? unique_prefix = setting_key.split("_")[0] : unique_prefix = unique_prefix;
|
||||
|
||||
|
||||
$.each(pluginsData, function (i, plgnObj){
|
||||
// go thru plugins
|
||||
if(plgnObj.unique_prefix == unique_prefix)
|
||||
{
|
||||
// go thru plugin settings
|
||||
$.each(plgnObj["settings"], function (j, setObj){
|
||||
|
||||
|
||||
if(`${unique_prefix}_${setObj.function}` == setting_key)
|
||||
{
|
||||
result = setObj
|
||||
{
|
||||
result = setObj
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1372,7 +1393,7 @@ function arraysContainSameValues(arr1, arr2) {
|
||||
if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
|
||||
return false;
|
||||
} else
|
||||
{
|
||||
{
|
||||
// Sort and stringify arrays, then compare
|
||||
return JSON.stringify(arr1.slice().sort()) === JSON.stringify(arr2.slice().sort());
|
||||
}
|
||||
@@ -1383,7 +1404,7 @@ function arraysContainSameValues(arr1, arr2) {
|
||||
function hideUIelements(setKey) {
|
||||
|
||||
hiddenSectionsSetting = getSetting(setKey)
|
||||
|
||||
|
||||
if(hiddenSectionsSetting != "") // handle if settings not yet initialized
|
||||
{
|
||||
|
||||
@@ -1398,9 +1419,9 @@ function hideUIelements(setKey) {
|
||||
|
||||
if($('#' + hiddenSection))
|
||||
{
|
||||
$('#' + hiddenSection).hide()
|
||||
}
|
||||
|
||||
$('#' + hiddenSection).hide()
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1411,7 +1432,7 @@ function getDevicesList()
|
||||
{
|
||||
// Read cache (skip cookie expiry check)
|
||||
devicesList = getCache('devicesListAll_JSON', true);
|
||||
|
||||
|
||||
if (devicesList != '') {
|
||||
devicesList = JSON.parse (devicesList);
|
||||
} else {
|
||||
@@ -1468,7 +1489,7 @@ $(document).ready(function() {
|
||||
// Restart Backend Python Server
|
||||
|
||||
function askRestartBackend() {
|
||||
// Ask
|
||||
// Ask
|
||||
showModalWarning(getString('Maint_RestartServer'), getString('Maint_Restart_Server_noti_text'),
|
||||
getString('Gen_Cancel'), getString('Maint_RestartServer'), 'restartBackend');
|
||||
}
|
||||
@@ -1477,7 +1498,7 @@ function askRestartBackend() {
|
||||
function restartBackend() {
|
||||
|
||||
modalEventStatusId = 'modal-message-front-event'
|
||||
|
||||
|
||||
// Execute
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
@@ -1523,7 +1544,7 @@ function clearCache() {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Function to check if cache needs to be refreshed because of setting changes
|
||||
function checkSettingChanges() {
|
||||
$.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now() }, function(appState) {
|
||||
$.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now() }, function(appState) {
|
||||
const importedMilliseconds = parseInt(appState["settingsImported"] * 1000);
|
||||
const lastReloaded = parseInt(sessionStorage.getItem(sessionStorageKey + '_time'));
|
||||
|
||||
@@ -1594,7 +1615,7 @@ function isAppInitialized() {
|
||||
|
||||
lang_shouldBeCompletedCalls = getLangCode() == 'en_us' ? 1 : 2;
|
||||
|
||||
// check if each ajax call completed succesfully
|
||||
// check if each ajax call completed succesfully
|
||||
$.each(completedCalls_final, function(index, call_name){
|
||||
|
||||
if(getCache(call_name + "_completed") != "true")
|
||||
@@ -1622,15 +1643,14 @@ async function executeOnce() {
|
||||
|
||||
if (!isAppInitialized()) {
|
||||
try {
|
||||
console.log("HERE");
|
||||
|
||||
|
||||
await waitForGraphQLServer(); // Wait for the server to start
|
||||
|
||||
await cacheDevices();
|
||||
await cacheSettings();
|
||||
await cacheStrings();
|
||||
|
||||
console.log("✅ All AJAX callbacks have completed");
|
||||
console.log("All AJAX callbacks have completed");
|
||||
onAllCallsComplete();
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
@@ -1680,7 +1700,7 @@ const onAllCallsComplete = () => {
|
||||
// setTimeout(() => {
|
||||
// location.reload()
|
||||
// }, 10);
|
||||
|
||||
|
||||
} else {
|
||||
// If not all strings are initialized, retry initialization
|
||||
console.log('❌ Not all strings are initialized. Retrying...');
|
||||
@@ -1702,7 +1722,7 @@ const areAllStringsInitialized = () => {
|
||||
// Call the function to execute the code
|
||||
executeOnce();
|
||||
|
||||
// Set timer for regular UI refresh if enabled
|
||||
// Set timer for regular UI refresh if enabled
|
||||
setTimeout(() => {
|
||||
|
||||
// page refresh if configured
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<?php
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/modals.php';
|
||||
require 'php/templates/modals.php';
|
||||
?>
|
||||
|
||||
|
||||
<script>
|
||||
// show spinning icon
|
||||
showSpinner()
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<!-- Page ------------------------------------------------------------------ -->
|
||||
<div class="content-wrapper">
|
||||
<span class="helpIcon">
|
||||
<span class="helpIcon">
|
||||
<a target="_blank" href="https://github.com/jokob-sk/NetAlertX/blob/main/docs/NETWORK_TREE.md">
|
||||
<i class="fa fa-circle-question"></i>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<div id="toggleFilters" class="">
|
||||
<div id="toggleFilters" class="">
|
||||
<div class="checkbox icheck col-xs-12">
|
||||
<label>
|
||||
<input type="checkbox" name="showOffline" checked>
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<?= lang('Network_ShowOffline');?>
|
||||
<span id="showOfflineNumber">
|
||||
<!-- placeholder -->
|
||||
@@ -31,14 +31,14 @@
|
||||
<div class="checkbox icheck col-xs-12">
|
||||
<label>
|
||||
<input type="checkbox" name="showArchived">
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<?= lang('Network_ShowArchived');?>
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<?= lang('Network_ShowArchived');?>
|
||||
<span id="showArchivedNumber">
|
||||
<!-- placeholder -->
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="networkTree" class="drag">
|
||||
@@ -55,8 +55,8 @@
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<!-- Placeholder -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<section id="unassigned-devices-wrapper">
|
||||
<!-- Placeholder -->
|
||||
</section>
|
||||
@@ -69,7 +69,7 @@
|
||||
require 'php/templates/footer.php';
|
||||
?>
|
||||
|
||||
<script src="lib/treeviz/bundle.js"></script>
|
||||
<script src="lib/treeviz/bundle.js"></script>
|
||||
|
||||
<script defer>
|
||||
|
||||
@@ -78,12 +78,12 @@
|
||||
|
||||
// Create Top level tabs (List of network devices), explanation of the terminology below:
|
||||
//
|
||||
// Switch 1 (node)
|
||||
// Switch 1 (node)
|
||||
// /(p1) \ (p2) <----- port numbers
|
||||
// / \
|
||||
// Smart TV (leaf) Switch 2 (node (for the PC) and leaf (for Switch 1))
|
||||
// \
|
||||
// PC (leaf) <------- leafs are not included in this SQL query
|
||||
// PC (leaf) <------- leafs are not included in this SQL query
|
||||
const rawSql = `
|
||||
SELECT node_name, node_mac, online, node_type, node_ports_count, parent_mac, node_icon, node_alert
|
||||
FROM (
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
const portLabel = node.node_ports_count ? ` (${node.node_ports_count})` : '';
|
||||
const icon = atob(node.node_icon);
|
||||
const id = node.node_mac.replace(/:/g, '_');
|
||||
const id = node.node_mac.replace(/:/g, '_');
|
||||
|
||||
html += `
|
||||
<li class="networkNodeTabHeaders ${i === 0 ? 'active' : ''}">
|
||||
@@ -137,13 +137,13 @@
|
||||
renderNetworkTabContent(nodes);
|
||||
|
||||
// init selected (first) tab
|
||||
initTab();
|
||||
initTab();
|
||||
|
||||
// init selected node highlighting
|
||||
// init selected node highlighting
|
||||
initSelectedNodeHighlighting()
|
||||
|
||||
// Register events on tab change
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
initSelectedNodeHighlighting()
|
||||
});
|
||||
}
|
||||
@@ -205,10 +205,10 @@
|
||||
<hr/>
|
||||
<div class="box box-aqua box-body" id="connected">
|
||||
<h5>
|
||||
<i class="fa fa-sitemap fa-rotate-270"></i>
|
||||
<i class="fa fa-sitemap fa-rotate-270"></i>
|
||||
${getString('Network_Connected')}
|
||||
</h5>
|
||||
|
||||
|
||||
<div id="leafs_${id}" class="table-responsive"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,9 +234,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$container.html(wrapperHtml);
|
||||
|
||||
|
||||
const $table = $(`#${tableId}`);
|
||||
|
||||
const columns = [
|
||||
@@ -298,7 +298,7 @@
|
||||
title: getString('Device_TableHead_Vendor'),
|
||||
data: 'devVendor',
|
||||
width: '20%'
|
||||
}
|
||||
}
|
||||
].filter(Boolean);
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@
|
||||
function loadConnectedDevices(node_mac) {
|
||||
const sql = `
|
||||
SELECT devName, devMac, devLastIP, devVendor, devPresentLastScan, devAlertDown, devParentPort,
|
||||
CASE
|
||||
CASE
|
||||
WHEN devIsNew = 1 THEN 'New'
|
||||
WHEN devPresentLastScan = 1 THEN 'On-line'
|
||||
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
|
||||
@@ -371,7 +371,7 @@
|
||||
|
||||
const wrapperHtml = `
|
||||
<table class="table table-bordered table-striped node-leafs-table " id="table_leafs_${id}" data-node-mac="${node_mac}">
|
||||
|
||||
|
||||
</table>`;
|
||||
|
||||
loadDeviceTable({
|
||||
@@ -414,12 +414,12 @@
|
||||
$.get(apiUrl, function (data) {
|
||||
|
||||
console.log(data);
|
||||
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
const allDevices = parsed;
|
||||
|
||||
console.log(allDevices);
|
||||
|
||||
|
||||
|
||||
if (!allDevices || allDevices.length === 0) {
|
||||
showModalOK(getString('Gen_Warning'), getString('Network_NoDevices'));
|
||||
@@ -439,14 +439,17 @@
|
||||
{
|
||||
$('#showArchivedNumber').text(`(${archivedCount})`);
|
||||
}
|
||||
|
||||
|
||||
if(offlineCount > 0)
|
||||
{
|
||||
$('#showOfflineNumber').text(`(${offlineCount})`);
|
||||
}
|
||||
|
||||
// Now apply UI filter based on toggles
|
||||
// Now apply UI filter based on toggles (always keep root)
|
||||
const filteredDevices = allDevices.filter(device => {
|
||||
const isRoot = (device.devMac || '').toLowerCase() === 'internet';
|
||||
|
||||
if (isRoot) return true;
|
||||
if (!showArchived && parseInt(device.devIsArchived) === 1) return false;
|
||||
if (!showOffline && parseInt(device.devPresentLastScan) === 0) return false;
|
||||
return true;
|
||||
@@ -501,7 +504,7 @@ var visibleNodesCount = 0;
|
||||
var parentNodesCount = 0;
|
||||
var hiddenMacs = []; // hidden children
|
||||
var hiddenChildren = [];
|
||||
var deviceListGlobal = null;
|
||||
var deviceListGlobal = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Recursively get children nodes and build a tree
|
||||
@@ -521,13 +524,17 @@ function getChildren(node, list, path, visited = [])
|
||||
|
||||
// Loop through all items to find children of the current node
|
||||
for (var i in list) {
|
||||
if (list[i].devParentMAC.toLowerCase() == node.devMac.toLowerCase() && !hiddenMacs.includes(list[i].devParentMAC)) {
|
||||
const item = list[i];
|
||||
const parentMac = item.devParentMAC || ""; // null-safe
|
||||
const nodeMac = node.devMac || ""; // null-safe
|
||||
|
||||
visibleNodesCount++;
|
||||
if (parentMac != "" && parentMac.toLowerCase() == nodeMac.toLowerCase() && !hiddenMacs.includes(parentMac)) {
|
||||
|
||||
// Process children recursively, passing a copy of the visited list
|
||||
children.push(getChildren(list[i], list, path + ((path == "") ? "" : '|') + list[i].devParentMAC, visited));
|
||||
}
|
||||
visibleNodesCount++;
|
||||
|
||||
// Process children recursively, passing a copy of the visited list
|
||||
children.push(getChildren(list[i], list, path + ((path == "") ? "" : '|') + parentMac, visited));
|
||||
}
|
||||
}
|
||||
|
||||
// Track leaf and parent node counts
|
||||
@@ -537,7 +544,7 @@ function getChildren(node, list, path, visited = [])
|
||||
parentNodesCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
return {
|
||||
name: node.devName,
|
||||
path: path,
|
||||
mac: node.devMac,
|
||||
@@ -562,19 +569,37 @@ function getChildren(node, list, path, visited = [])
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
function getHierarchy()
|
||||
{
|
||||
{
|
||||
// reset counters before rebuilding the hierarchy
|
||||
leafNodesCount = 0;
|
||||
visibleNodesCount = 0;
|
||||
parentNodesCount = 0;
|
||||
|
||||
let internetNode = null;
|
||||
|
||||
for(i in deviceListGlobal)
|
||||
{
|
||||
{
|
||||
if(deviceListGlobal[i].devMac == 'Internet')
|
||||
{
|
||||
return (getChildren(deviceListGlobal[i], deviceListGlobal, ''))
|
||||
{
|
||||
internetNode = deviceListGlobal[i];
|
||||
|
||||
return (getChildren(internetNode, deviceListGlobal, ''))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!internetNode) {
|
||||
showModalOk(
|
||||
getString('Network_Configuration_Error'),
|
||||
getString('Network_Root_Not_Configured')
|
||||
);
|
||||
console.error("getHierarchy(): Internet node not found");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
function toggleSubTree(parentMac, treePath)
|
||||
{
|
||||
@@ -593,33 +618,33 @@ function toggleSubTree(parentMac, treePath)
|
||||
myTree.refresh(updatedTree);
|
||||
|
||||
// re-attach any onclick events
|
||||
attachTreeEvents();
|
||||
attachTreeEvents();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
function attachTreeEvents()
|
||||
{
|
||||
// toggle subtree functionality
|
||||
$("div[data-mytreemac]").each(function(){
|
||||
$(this).attr('onclick', 'toggleSubTree("'+$(this).attr('data-mytreemac')+'","'+ $(this).attr('data-mytreepath')+'")')
|
||||
});
|
||||
$(this).attr('onclick', 'toggleSubTree("'+$(this).attr('data-mytreemac')+'","'+ $(this).attr('data-mytreepath')+'")')
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handle network node click - select correct tab in the bottom table
|
||||
function handleNodeClick(el)
|
||||
{
|
||||
{
|
||||
|
||||
isNetworkDevice = $(el).data("devisnetworknodedynamic") == 1;
|
||||
targetTabMAC = ""
|
||||
thisDevMac= $(el).data("mac");
|
||||
|
||||
thisDevMac= $(el).data("mac");
|
||||
|
||||
if (isNetworkDevice == false)
|
||||
{
|
||||
targetTabMAC = $(el).data("parentmac");
|
||||
targetTabMAC = $(el).data("parentmac");
|
||||
} else
|
||||
{
|
||||
targetTabMAC = thisDevMac;
|
||||
targetTabMAC = thisDevMac;
|
||||
}
|
||||
|
||||
var targetTab = $(`a[data-mytabmac="${targetTabMAC}"]`);
|
||||
@@ -628,8 +653,8 @@ function handleNodeClick(el)
|
||||
// Simulate a click event on the target tab
|
||||
targetTab.click();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (isNetworkDevice) {
|
||||
// Smooth scroll to the tab content
|
||||
@@ -639,7 +664,7 @@ function handleNodeClick(el)
|
||||
} else {
|
||||
$("tr.selected").removeClass("selected");
|
||||
$(`tr[data-mac="${thisDevMac}"]`).addClass("selected");
|
||||
|
||||
|
||||
const tableId = "table_leafs_" + targetTabMAC.replace(/:/g, '_');
|
||||
const $table = $(`#${tableId}`).DataTable();
|
||||
|
||||
@@ -669,10 +694,8 @@ function handleNodeClick(el)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
var myTree;
|
||||
|
||||
|
||||
var emSize;
|
||||
var nodeHeight;
|
||||
// var sizeCoefficient = 1.4
|
||||
@@ -689,140 +712,150 @@ function emToPx(em, element) {
|
||||
|
||||
function initTree(myHierarchy)
|
||||
{
|
||||
// calculate the drawing area based on teh tree width and available screen size
|
||||
|
||||
let baseFontSize = parseFloat($('html').css('font-size'));
|
||||
let treeAreaHeight = ($(window).height() - 155); ;
|
||||
// calculate the font size of the leaf nodes to fit everything into the tree area
|
||||
leafNodesCount == 0 ? 1 : leafNodesCount;
|
||||
|
||||
emSize = pxToEm((treeAreaHeight/(leafNodesCount)).toFixed(2));
|
||||
|
||||
let screenWidthEm = pxToEm($('.networkTable').width()-15);
|
||||
|
||||
// init the drawing area size
|
||||
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${emToPx(screenWidthEm)}px`)
|
||||
|
||||
if(myHierarchy.type == "")
|
||||
if(myHierarchy && myHierarchy.type !== "")
|
||||
{
|
||||
showModalOk(getString('Network_Configuration_Error'), getString('Network_Root_Not_Configured'))
|
||||
|
||||
return;
|
||||
// calculate the drawing area based on the tree width and available screen size
|
||||
let baseFontSize = parseFloat($('html').css('font-size'));
|
||||
let treeAreaHeight = ($(window).height() - 155); ;
|
||||
let minNodeWidth = 60 // min safe node width not breaking the tree
|
||||
|
||||
// calculate the font size of the leaf nodes to fit everything into the tree area
|
||||
leafNodesCount == 0 ? 1 : leafNodesCount;
|
||||
|
||||
emSize = pxToEm((treeAreaHeight/(leafNodesCount)).toFixed(2));
|
||||
|
||||
// let screenWidthEm = pxToEm($('.networkTable').width()-15);
|
||||
let minTreeWidthPx = parentNodesCount * minNodeWidth;
|
||||
let actualWidthPx = $('.networkTable').width() - 15;
|
||||
|
||||
let finalWidthPx = Math.max(actualWidthPx, minTreeWidthPx);
|
||||
|
||||
// override original value
|
||||
let screenWidthEm = pxToEm(finalWidthPx);
|
||||
|
||||
// handle canvas and node size if only a few nodes
|
||||
emSize > 1 ? emSize = 1 : emSize = emSize;
|
||||
|
||||
let nodeHeightPx = emToPx(emSize*1);
|
||||
let nodeWidthPx = emToPx(screenWidthEm / (parentNodesCount));
|
||||
|
||||
// handle if only a few nodes
|
||||
nodeWidthPx > 160 ? nodeWidthPx = 160 : nodeWidthPx = nodeWidthPx;
|
||||
if (nodeWidthPx < minNodeWidth) nodeWidthPx = minNodeWidth; // minimum safe width
|
||||
|
||||
console.log("Calculated nodeWidthPx =", nodeWidthPx, "emSize =", emSize , " screenWidthEm:", screenWidthEm, " emToPx(screenWidthEm):" , emToPx(screenWidthEm));
|
||||
|
||||
// init the drawing area size
|
||||
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${emToPx(screenWidthEm)}px`)
|
||||
|
||||
console.log(Treeviz);
|
||||
|
||||
myTree = Treeviz.create({
|
||||
htmlId: "networkTree",
|
||||
renderNode: nodeData => {
|
||||
|
||||
(!emptyArr.includes(nodeData.data.port )) ? port = nodeData.data.port : port = "";
|
||||
|
||||
(port == "" || port == 0 || port == 'None' ) ? portBckgIcon = `<i class="fa fa-wifi"></i>` : portBckgIcon = `<i class="fa fa-ethernet"></i>`;
|
||||
|
||||
portHtml = (port == "" || port == 0 || port == 'None' ) ? "   " : port;
|
||||
|
||||
// Build HTML for individual nodes in the network diagram
|
||||
deviceIcon = (!emptyArr.includes(nodeData.data.icon )) ?
|
||||
`<div class="netIcon">
|
||||
${atob(nodeData.data.icon)}
|
||||
</div>` : "";
|
||||
devicePort = `<div class="netPort"
|
||||
style="width:${emSize}em;height:${emSize}em">
|
||||
${portHtml}</div>
|
||||
<div class="portBckgIcon"
|
||||
style="margin-left:-${emSize*0.7}em;">
|
||||
${portBckgIcon}
|
||||
</div>`;
|
||||
collapseExpandIcon = nodeData.data.hiddenChildren ?
|
||||
"square-plus" : "square-minus";
|
||||
|
||||
// generate +/- icon if node has children nodes
|
||||
collapseExpandHtml = nodeData.data.hasChildren ?
|
||||
`<div class="netCollapse"
|
||||
style="font-size:${nodeHeightPx/2}px;top:${Math.floor(nodeHeightPx / 4)}px"
|
||||
data-mytreepath="${nodeData.data.path}"
|
||||
data-mytreemac="${nodeData.data.mac}">
|
||||
<i class="fa fa-${collapseExpandIcon} pointer"></i>
|
||||
</div>` : "";
|
||||
|
||||
selectedNodeMac = $(".nav-tabs-custom .active a").attr('data-mytabmac')
|
||||
|
||||
highlightedCss = nodeData.data.mac == selectedNodeMac ?
|
||||
" highlightedNode " : "";
|
||||
cssNodeType = nodeData.data.devIsNetworkNodeDynamic ?
|
||||
" node-network-device " : " node-standard-device ";
|
||||
|
||||
networkHardwareIcon = nodeData.data.devIsNetworkNodeDynamic ? `<span class="network-hw-icon">
|
||||
<i class="fa-solid fa-hard-drive"></i>
|
||||
</span>` : "";
|
||||
|
||||
const badgeConf = getStatusBadgeParts(nodeData.data.presentLastScan, nodeData.data.alertDown, nodeData.data.mac, statusText = '')
|
||||
|
||||
return result = `<div
|
||||
class="node-inner hover-node-info box pointer ${highlightedCss} ${cssNodeType}"
|
||||
style="height:${nodeHeightPx}px;font-size:${nodeHeightPx-5}px;"
|
||||
onclick="handleNodeClick(this)"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-parentMac="${nodeData.data.parentMac}"
|
||||
data-name="${nodeData.data.name}"
|
||||
data-ip="${nodeData.data.ip}"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-vendor="${nodeData.data.vendor}"
|
||||
data-type="${nodeData.data.type}"
|
||||
data-devIsNetworkNodeDynamic="${nodeData.data.devIsNetworkNodeDynamic}"
|
||||
data-lastseen="${nodeData.data.lastseen}"
|
||||
data-firstseen="${nodeData.data.firstseen}"
|
||||
data-relationship="${nodeData.data.relType}"
|
||||
data-status="${nodeData.data.status}"
|
||||
data-present="${nodeData.data.presentLastScan}"
|
||||
data-alert="${nodeData.data.alertDown}"
|
||||
data-icon="${nodeData.data.icon}"
|
||||
>
|
||||
<div class="netNodeText">
|
||||
<strong><span>${devicePort} <span class="${badgeConf.cssText}">${deviceIcon}</span></span>
|
||||
<span class="spanNetworkTree anonymizeDev" style="width:${nodeWidthPx-50}px">${nodeData.data.name}</span>
|
||||
${networkHardwareIcon}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
${collapseExpandHtml}`;
|
||||
},
|
||||
mainAxisNodeSpacing: 'auto',
|
||||
// secondaryAxisNodeSpacing: 0.3,
|
||||
nodeHeight: nodeHeightPx,
|
||||
nodeWidth: nodeWidthPx,
|
||||
marginTop: '5',
|
||||
isHorizontal : true,
|
||||
hasZoom: true,
|
||||
hasPan: true,
|
||||
marginLeft: '10',
|
||||
marginRight: '10',
|
||||
idKey: "mac",
|
||||
hasFlatData: false,
|
||||
relationnalField: "children",
|
||||
linkWidth: (nodeData) => 2,
|
||||
linkColor: (nodeData) => {
|
||||
relConf = getRelationshipConf(nodeData.data.relType)
|
||||
return relConf.color;
|
||||
}
|
||||
// onNodeClick: (nodeData) => handleNodeClick(nodeData),
|
||||
});
|
||||
|
||||
console.log(deviceListGlobal);
|
||||
myTree.refresh(myHierarchy);
|
||||
|
||||
// hide spinning icon
|
||||
hideSpinner()
|
||||
} else
|
||||
{
|
||||
console.error("getHierarchy() not returning expected result");
|
||||
}
|
||||
|
||||
// handle canvas and node size if only a few nodes
|
||||
emSize > 1 ? emSize = 1 : emSize = emSize;
|
||||
|
||||
let nodeHeightPx = emToPx(emSize*1);
|
||||
let nodeWidthPx = emToPx(screenWidthEm / (parentNodesCount));
|
||||
|
||||
// handle if only a few nodes
|
||||
nodeWidthPx > 160 ? nodeWidthPx = 160 : nodeWidthPx = nodeWidthPx;
|
||||
|
||||
console.log(Treeviz);
|
||||
|
||||
myTree = Treeviz.create({
|
||||
htmlId: "networkTree",
|
||||
renderNode: nodeData => {
|
||||
|
||||
(!emptyArr.includes(nodeData.data.port )) ? port = nodeData.data.port : port = "";
|
||||
|
||||
(port == "" || port == 0 || port == 'None' ) ? portBckgIcon = `<i class="fa fa-wifi"></i>` : portBckgIcon = `<i class="fa fa-ethernet"></i>`;
|
||||
|
||||
portHtml = (port == "" || port == 0 || port == 'None' ) ? "   " : port;
|
||||
|
||||
// Build HTML for individual nodes in the network diagram
|
||||
deviceIcon = (!emptyArr.includes(nodeData.data.icon )) ?
|
||||
`<div class="netIcon">
|
||||
${atob(nodeData.data.icon)}
|
||||
</div>` : "";
|
||||
devicePort = `<div class="netPort"
|
||||
style="width:${emSize}em;height:${emSize}em">
|
||||
${portHtml}</div>
|
||||
<div class="portBckgIcon"
|
||||
style="margin-left:-${emSize*0.7}em;">
|
||||
${portBckgIcon}
|
||||
</div>`;
|
||||
collapseExpandIcon = nodeData.data.hiddenChildren ?
|
||||
"square-plus" : "square-minus";
|
||||
|
||||
// generate +/- icon if node has children nodes
|
||||
collapseExpandHtml = nodeData.data.hasChildren ?
|
||||
`<div class="netCollapse"
|
||||
style="font-size:${nodeHeightPx/2}px;top:${Math.floor(nodeHeightPx / 4)}px"
|
||||
data-mytreepath="${nodeData.data.path}"
|
||||
data-mytreemac="${nodeData.data.mac}">
|
||||
<i class="fa fa-${collapseExpandIcon} pointer"></i>
|
||||
</div>` : "";
|
||||
|
||||
selectedNodeMac = $(".nav-tabs-custom .active a").attr('data-mytabmac')
|
||||
|
||||
highlightedCss = nodeData.data.mac == selectedNodeMac ?
|
||||
" highlightedNode " : "";
|
||||
cssNodeType = nodeData.data.devIsNetworkNodeDynamic ?
|
||||
" node-network-device " : " node-standard-device ";
|
||||
|
||||
networkHardwareIcon = nodeData.data.devIsNetworkNodeDynamic ? `<span class="network-hw-icon">
|
||||
<i class="fa-solid fa-hard-drive"></i>
|
||||
</span>` : "";
|
||||
|
||||
const badgeConf = getStatusBadgeParts(nodeData.data.presentLastScan, nodeData.data.alertDown, nodeData.data.mac, statusText = '')
|
||||
|
||||
return result = `<div
|
||||
class="node-inner hover-node-info box pointer ${highlightedCss} ${cssNodeType}"
|
||||
style="height:${nodeHeightPx}px;font-size:${nodeHeightPx-5}px;"
|
||||
onclick="handleNodeClick(this)"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-parentMac="${nodeData.data.parentMac}"
|
||||
data-name="${nodeData.data.name}"
|
||||
data-ip="${nodeData.data.ip}"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-vendor="${nodeData.data.vendor}"
|
||||
data-type="${nodeData.data.type}"
|
||||
data-devIsNetworkNodeDynamic="${nodeData.data.devIsNetworkNodeDynamic}"
|
||||
data-lastseen="${nodeData.data.lastseen}"
|
||||
data-firstseen="${nodeData.data.firstseen}"
|
||||
data-relationship="${nodeData.data.relType}"
|
||||
data-status="${nodeData.data.status}"
|
||||
data-present="${nodeData.data.presentLastScan}"
|
||||
data-alert="${nodeData.data.alertDown}"
|
||||
data-icon="${nodeData.data.icon}"
|
||||
>
|
||||
<div class="netNodeText">
|
||||
<strong><span>${devicePort} <span class="${badgeConf.cssText}">${deviceIcon}</span></span>
|
||||
<span class="spanNetworkTree anonymizeDev" style="width:${nodeWidthPx-50}px">${nodeData.data.name}</span>
|
||||
${networkHardwareIcon}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
${collapseExpandHtml}`;
|
||||
},
|
||||
mainAxisNodeSpacing: 'auto',
|
||||
// secondaryAxisNodeSpacing: 0.3,
|
||||
nodeHeight: nodeHeightPx,
|
||||
nodeWidth: nodeWidthPx,
|
||||
marginTop: '5',
|
||||
isHorizontal : true,
|
||||
hasZoom: true,
|
||||
hasPan: true,
|
||||
marginLeft: '10',
|
||||
marginRight: '10',
|
||||
idKey: "mac",
|
||||
hasFlatData: false,
|
||||
relationnalField: "children",
|
||||
linkWidth: (nodeData) => 2,
|
||||
linkColor: (nodeData) => {
|
||||
relConf = getRelationshipConf(nodeData.data.relType)
|
||||
return relConf.color;
|
||||
}
|
||||
// onNodeClick: (nodeData) => handleNodeClick(nodeData),
|
||||
});
|
||||
|
||||
console.log(deviceListGlobal);
|
||||
myTree.refresh(myHierarchy);
|
||||
|
||||
// hide spinning icon
|
||||
hideSpinner()
|
||||
}
|
||||
|
||||
|
||||
@@ -839,11 +872,11 @@ function initTab()
|
||||
selectedTab = "Internet_id"
|
||||
|
||||
// the #target from the url
|
||||
target = getQueryString('mac')
|
||||
target = getQueryString('mac')
|
||||
|
||||
// update cookie if target specified
|
||||
if(target != "")
|
||||
{
|
||||
{
|
||||
setCache(key, target.replaceAll(":","_")+'_id') // _id is added so it doesn't conflict with AdminLTE tab behavior
|
||||
}
|
||||
|
||||
@@ -860,12 +893,12 @@ function initTab()
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
setCache(key, $(e.target).attr('id'))
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
function initSelectedNodeHighlighting()
|
||||
{
|
||||
{
|
||||
|
||||
var currentNodeMac = $(".networkNodeTabHeaders.active a").data("mytabmac");
|
||||
|
||||
@@ -882,7 +915,7 @@ function initSelectedNodeHighlighting()
|
||||
newSelNode = $("#networkTree div[data-mac='"+currentNodeMac+"']")[0]
|
||||
|
||||
console.log(newSelNode)
|
||||
|
||||
|
||||
$(newSelNode).attr('class', $(newSelNode).attr('class') + ' highlightedNode')
|
||||
}
|
||||
|
||||
@@ -913,7 +946,7 @@ function updateLeaf(leafMac, action) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// showing icons or device names in tabs depending on available screen size
|
||||
// showing icons or device names in tabs depending on available screen size
|
||||
function checkTabsOverflow() {
|
||||
const $ul = $('.nav-tabs');
|
||||
const $lis = $ul.find('li');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
//------------------------------------------------------------------------------
|
||||
// NetAlertX
|
||||
// Open Source Network Guard / WIFI & LAN intrusion detector
|
||||
// Open Source Network Guard / WIFI & LAN intrusion detector
|
||||
//
|
||||
// util.php - Front module. Server side. Common generic functions
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -22,8 +22,8 @@ $ACTION = "";
|
||||
|
||||
// init request params
|
||||
if(array_key_exists('function', $_REQUEST) != FALSE)
|
||||
{
|
||||
$FUNCTION = $_REQUEST['function'];
|
||||
{
|
||||
$FUNCTION = $_REQUEST['function'];
|
||||
}
|
||||
if(array_key_exists('settings', $_REQUEST) != FALSE)
|
||||
{
|
||||
@@ -33,13 +33,13 @@ if(array_key_exists('settings', $_REQUEST) != FALSE)
|
||||
|
||||
// call functions based on requested params
|
||||
switch ($FUNCTION) {
|
||||
|
||||
|
||||
case 'savesettings':
|
||||
|
||||
|
||||
saveSettings();
|
||||
break;
|
||||
|
||||
case 'cleanLog':
|
||||
case 'cleanLog':
|
||||
|
||||
cleanLog($SETTINGS);
|
||||
break;
|
||||
@@ -66,7 +66,7 @@ switch ($FUNCTION) {
|
||||
// Creates a PHP array from a string representing a python array (input format ['...','...'])
|
||||
// Only supports:
|
||||
// - one level arrays, not nested ones
|
||||
// - single quotes
|
||||
// - single quotes
|
||||
function createArray($input){
|
||||
|
||||
// empty array
|
||||
@@ -81,9 +81,9 @@ function createArray($input){
|
||||
$replacement = '';
|
||||
|
||||
// remove brackets
|
||||
$noBrackets = preg_replace($patternBrackets, $replacement, $input);
|
||||
|
||||
$options = array();
|
||||
$noBrackets = preg_replace($patternBrackets, $replacement, $input);
|
||||
|
||||
$options = array();
|
||||
|
||||
// create array
|
||||
$optionsTmp = explode(",", $noBrackets);
|
||||
@@ -99,7 +99,7 @@ function createArray($input){
|
||||
{
|
||||
array_push($options, preg_replace($patternQuotes, $replacement, $item) );
|
||||
}
|
||||
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function printArray ($array) {
|
||||
{
|
||||
echo $val.', ';
|
||||
}
|
||||
}
|
||||
}
|
||||
echo ']<br/>';
|
||||
}
|
||||
|
||||
@@ -171,9 +171,9 @@ function checkPermissions($files)
|
||||
if(file_exists($file) != 1)
|
||||
{
|
||||
$message = "File '".$file."' not found or inaccessible. Correct file permissions, create one yourself or generate a new one in 'Settings' by clicking the 'Save' button.";
|
||||
displayMessage($message, TRUE);
|
||||
displayMessage($message, TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------
|
||||
@@ -189,8 +189,8 @@ function displayMessage($message, $logAlert = FALSE, $logConsole = TRUE, $logFil
|
||||
$message = str_replace(array("\n", "\r", PHP_EOL), '', $message);
|
||||
|
||||
echo "<script>function escape(html, encode) {
|
||||
return html.replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||
.replace(/\t/g, '')
|
||||
return html.replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||
.replace(/\t/g, '')
|
||||
}</script>";
|
||||
|
||||
// Javascript Alert pop-up
|
||||
@@ -210,7 +210,7 @@ function displayMessage($message, $logAlert = FALSE, $logConsole = TRUE, $logFil
|
||||
{
|
||||
|
||||
if (is_writable($logFolderPath.$log_file)) {
|
||||
|
||||
|
||||
|
||||
if(file_exists($logFolderPath.$log_file) != 1) // file doesn't exist, create one
|
||||
{
|
||||
@@ -219,7 +219,7 @@ function displayMessage($message, $logAlert = FALSE, $logConsole = TRUE, $logFil
|
||||
{
|
||||
$log = fopen($logFolderPath.$log_file, "a") or die("Unable to open file - Permissions issue!");
|
||||
}
|
||||
|
||||
|
||||
fwrite($log, "[".$timestamp. "] " . str_replace('<br>',"\n ",str_replace('<br/>',"\n ",$message)).PHP_EOL."" );
|
||||
fclose($log);
|
||||
|
||||
@@ -269,13 +269,13 @@ function addToExecutionQueue($action)
|
||||
// equivalent: /logs DELETE
|
||||
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
|
||||
function cleanLog($logFile)
|
||||
{
|
||||
{
|
||||
global $logFolderPath, $timestamp;
|
||||
|
||||
$path = "";
|
||||
|
||||
$allowedFiles = ['app.log', 'app_front.log', 'IP_changes.log', 'stdout.log', 'stderr.log', 'app.php_errors.log', 'execution_queue.log', 'db_is_locked.log', 'nginx-error.log', 'cron.log'];
|
||||
|
||||
|
||||
if(in_array($logFile, $allowedFiles))
|
||||
{
|
||||
$path = $logFolderPath.$logFile;
|
||||
@@ -287,11 +287,11 @@ function cleanLog($logFile)
|
||||
$file = fopen($path, "w") or die("Unable to open file!");
|
||||
fwrite($file, "");
|
||||
fclose($file);
|
||||
displayMessage('File <code>'.$logFile.'</code> purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
displayMessage('File <code>'.$logFile.'</code> purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
} else
|
||||
{
|
||||
displayMessage('File <code>'.$logFile.'</code> is not allowed to be purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
displayMessage('File <code>'.$logFile.'</code> is not allowed to be purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -299,23 +299,23 @@ function cleanLog($logFile)
|
||||
// ----------------------------------------------------------------------------------------
|
||||
function saveSettings()
|
||||
{
|
||||
global $SETTINGS, $FUNCTION, $config_file, $fullConfPath, $configFolderPath, $timestamp;
|
||||
global $SETTINGS, $FUNCTION, $config_file, $fullConfPath, $configFolderPath, $timestamp;
|
||||
|
||||
// save to the file
|
||||
$new_name = $config_file.'_'.$timestamp.'.backup';
|
||||
$new_location = $configFolderPath.$new_name;
|
||||
$new_location = $configFolderPath.'/'.$new_name;
|
||||
|
||||
if(file_exists( $fullConfPath) != 1)
|
||||
{
|
||||
displayMessage('File "'.$fullConfPath.'" not found or missing read permissions. Creating a new <code>'.$config_file.'</code> file.', FALSE, TRUE, TRUE, TRUE);
|
||||
{
|
||||
displayMessage('File "'.$fullConfPath.'" not found or missing read permissions. Creating a new <code>'.$config_file.'</code> file.', FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
// create a backup copy
|
||||
// create a backup copy
|
||||
elseif (!copy($fullConfPath, $new_location))
|
||||
{
|
||||
displayMessage("Failed to copy file ".$fullConfPath." to ".$new_location." <br/> Check your permissions to allow read/write access to the /config folder.", FALSE, TRUE, TRUE, TRUE);
|
||||
{
|
||||
displayMessage("Failed to copy file ".$fullConfPath." to ".$new_location." <br/> Check your permissions to allow read/write access to the /config folder.", FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// generate a clean .conf file
|
||||
$groups = [];
|
||||
|
||||
@@ -339,12 +339,12 @@ function saveSettings()
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($decodedSettings as $setting) {
|
||||
foreach ($decodedSettings as $setting) {
|
||||
if( in_array($setting[0] , $groups) == false) {
|
||||
array_push($groups ,$setting[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// go thru the groups and prepare settings to write to file
|
||||
foreach ($groups as $group) {
|
||||
$txt .= "\n\n# " . $group;
|
||||
@@ -353,20 +353,20 @@ function saveSettings()
|
||||
foreach ($decodedSettings as $setting) {
|
||||
$settingGroup = $setting[0];
|
||||
$setKey = $setting[1];
|
||||
$dataType = $setting[2];
|
||||
$dataType = $setting[2];
|
||||
$settingValue = $setting[3];
|
||||
|
||||
|
||||
// // Parse the settingType JSON
|
||||
// $settingType = json_decode($settingTypeJson, true);
|
||||
|
||||
|
||||
// Sanity check
|
||||
if($setKey == "UI_LANG" && $settingValue == "") {
|
||||
echo "🔴 Error: important settings missing. Refresh the page with 🔃 on the top and try again.";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($group == $settingGroup) {
|
||||
|
||||
|
||||
if ($group == $settingGroup) {
|
||||
|
||||
if ($dataType == 'string' ) {
|
||||
$val = encode_single_quotes($settingValue);
|
||||
$txt .= $setKey . "='" . $val . "'\n";
|
||||
@@ -381,7 +381,7 @@ function saveSettings()
|
||||
$txt .= $setKey . "=" . $val . "\n";
|
||||
} elseif ($dataType == 'array' ) {
|
||||
$temp = '';
|
||||
|
||||
|
||||
if(is_array($settingValue) == FALSE)
|
||||
{
|
||||
$settingValue = json_decode($settingValue);
|
||||
@@ -397,22 +397,22 @@ function saveSettings()
|
||||
|
||||
$temp = '['.$temp.']'; // wrap brackets
|
||||
$txt .= $setKey . "=" . $temp . "\n";
|
||||
|
||||
|
||||
} else {
|
||||
$txt .= $setKey . "='⭕Not handled⭕'\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
$txt = $txt."\n\n";
|
||||
$txt = $txt."#-------------------IMPORTANT INFO-------------------#\n";
|
||||
$txt = $txt."# This file is ingested by a python script, so if #\n";
|
||||
$txt = $txt."# modified it needs to use python syntax #\n";
|
||||
$txt = $txt."# This file is ingested by a python script, so if #\n";
|
||||
$txt = $txt."# modified it needs to use python syntax #\n";
|
||||
$txt = $txt."#-------------------IMPORTANT INFO-------------------#\n";
|
||||
|
||||
// open new file and write the new configuration
|
||||
// open new file and write the new configuration
|
||||
// Create a temporary file
|
||||
$tempConfPath = $fullConfPath . ".tmp";
|
||||
|
||||
@@ -426,8 +426,8 @@ function saveSettings()
|
||||
fwrite($file, $txt);
|
||||
fclose($file);
|
||||
|
||||
// displayMessage(lang('settings_saved'),
|
||||
// FALSE, TRUE, TRUE, TRUE);
|
||||
// displayMessage(lang('settings_saved'),
|
||||
// FALSE, TRUE, TRUE, TRUE);
|
||||
|
||||
echo "OK";
|
||||
|
||||
@@ -445,7 +445,7 @@ function getString ($setKey, $default) {
|
||||
if ($result )
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
@@ -520,14 +520,14 @@ function getDateFromPeriod () {
|
||||
$days = "3650"; //10 years
|
||||
break;
|
||||
default:
|
||||
$days = "1";
|
||||
}
|
||||
$days = "1";
|
||||
}
|
||||
|
||||
$periodDateSQL = "-".$days." day";
|
||||
$periodDateSQL = "-".$days." day";
|
||||
|
||||
return " date('now', '".$periodDateSQL."') ";
|
||||
|
||||
// $period = $_REQUEST['period'];
|
||||
|
||||
// $period = $_REQUEST['period'];
|
||||
// return '"'. date ('Y-m-d', strtotime ('+2 day -'. $period) ) .'"';
|
||||
}
|
||||
|
||||
@@ -537,13 +537,13 @@ function getDateFromPeriod () {
|
||||
function quotes ($text) {
|
||||
return str_replace ('"','""',$text);
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
function logServerConsole ($text) {
|
||||
$x = array();
|
||||
$y = $x['__________'. $text .'__________'];
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
function handleNull ($text, $default = "") {
|
||||
if($text == NULL || $text == 'NULL')
|
||||
@@ -553,7 +553,7 @@ function handleNull ($text, $default = "") {
|
||||
{
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
@@ -581,14 +581,14 @@ function decodeSpecialChars($str) {
|
||||
// used in Export CSV
|
||||
function getDevicesColumns(){
|
||||
|
||||
$columns = ["devMac",
|
||||
$columns = ["devMac",
|
||||
"devName",
|
||||
"devOwner",
|
||||
"devType",
|
||||
"devVendor",
|
||||
"devFavorite",
|
||||
"devGroup",
|
||||
"devComments",
|
||||
"devComments",
|
||||
"devFirstConnection",
|
||||
"devLastConnection",
|
||||
"devLastIP",
|
||||
@@ -615,8 +615,8 @@ function getDevicesColumns(){
|
||||
"devFQDN",
|
||||
"devParentRelType",
|
||||
"devReqNicsOnline"
|
||||
];
|
||||
|
||||
];
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -646,7 +646,7 @@ function getCache($key) {
|
||||
}
|
||||
// -------------------------------------------------------------------------------------------
|
||||
function setCache($key, $value, $expireMinutes = 5) {
|
||||
setcookie($key, $value, time()+$expireMinutes*60, "/","", 0);
|
||||
setcookie($key, $value, time()+$expireMinutes*60, "/","", 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
4
front/php/templates/language/fr_fr.json
Executable file → Normal file
@@ -311,7 +311,7 @@
|
||||
"Gen_Filter": "Filtrer",
|
||||
"Gen_Generate": "Générer",
|
||||
"Gen_InvalidMac": "Adresse MAC invalide.",
|
||||
"Gen_Invalid_Value": "",
|
||||
"Gen_Invalid_Value": "Une valeur invalide a été renseignée",
|
||||
"Gen_LockedDB": "Erreur - La base de données est peut-être verrouillée - Vérifier avec les outils de dév via F12 -> Console ou essayer plus tard.",
|
||||
"Gen_NetworkMask": "Masque réseau",
|
||||
"Gen_Offline": "Hors ligne",
|
||||
@@ -762,4 +762,4 @@
|
||||
"settings_system_label": "Système",
|
||||
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. <b>Il n'y a pas de pas de contrôle.</b>",
|
||||
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
|
||||
}
|
||||
}
|
||||
|
||||
4
front/php/templates/language/it_it.json
Executable file → Normal file
@@ -311,7 +311,7 @@
|
||||
"Gen_Filter": "Filtro",
|
||||
"Gen_Generate": "Genera",
|
||||
"Gen_InvalidMac": "Indirizzo Mac non valido.",
|
||||
"Gen_Invalid_Value": "",
|
||||
"Gen_Invalid_Value": "È stato inserito un valore non valido",
|
||||
"Gen_LockedDB": "ERRORE: il DB potrebbe essere bloccato, controlla F12 Strumenti di sviluppo -> Console o riprova più tardi.",
|
||||
"Gen_NetworkMask": "Maschera di rete",
|
||||
"Gen_Offline": "Offline",
|
||||
@@ -762,4 +762,4 @@
|
||||
"settings_system_label": "Sistema",
|
||||
"settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. <b>La convalida non viene eseguita.</b>",
|
||||
"test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@
|
||||
"Gen_Filter": "Фильтр",
|
||||
"Gen_Generate": "Генерировать",
|
||||
"Gen_InvalidMac": "Неверный Mac-адрес.",
|
||||
"Gen_Invalid_Value": "",
|
||||
"Gen_Invalid_Value": "Введено некорректное значение",
|
||||
"Gen_LockedDB": "ОШИБКА - Возможно, база данных заблокирована. Проверьте инструменты разработчика F12 -> Консоль или повторите попытку позже.",
|
||||
"Gen_NetworkMask": "Маска сети",
|
||||
"Gen_Offline": "Оффлайн",
|
||||
@@ -762,4 +762,4 @@
|
||||
"settings_system_label": "Система",
|
||||
"settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. <b>Проверка не выполняется.</b>",
|
||||
"test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки."
|
||||
}
|
||||
}
|
||||
|
||||
4
front/php/templates/language/uk_ua.json
Executable file → Normal file
@@ -311,7 +311,7 @@
|
||||
"Gen_Filter": "Фільтр",
|
||||
"Gen_Generate": "Генерувати",
|
||||
"Gen_InvalidMac": "Недійсна Mac-адреса.",
|
||||
"Gen_Invalid_Value": "",
|
||||
"Gen_Invalid_Value": "Введено недійсне значення",
|
||||
"Gen_LockedDB": "ПОМИЛКА – БД може бути заблоковано – перевірте F12 Інструменти розробника -> Консоль або спробуйте пізніше.",
|
||||
"Gen_NetworkMask": "Маска мережі",
|
||||
"Gen_Offline": "Офлайн",
|
||||
@@ -762,4 +762,4 @@
|
||||
"settings_system_label": "Система",
|
||||
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. <b>Перевірка не виконана.</b>",
|
||||
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import hashlib
|
||||
import re
|
||||
import nmap
|
||||
|
||||
@@ -17,6 +16,7 @@ from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
|
||||
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from const import logPath # noqa: E402 [flake8 lint suppression]
|
||||
from utils.crypto_utils import string_to_mac_hash # noqa: E402 [flake8 lint suppression]
|
||||
import conf # noqa: E402 [flake8 lint suppression]
|
||||
from pytz import timezone # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
@@ -177,16 +177,6 @@ def parse_nmap_xml(xml_output, interface, fakeMac):
|
||||
return devices_list
|
||||
|
||||
|
||||
def string_to_mac_hash(input_string):
|
||||
# Calculate a hash using SHA-256
|
||||
sha256_hash = hashlib.sha256(input_string.encode()).hexdigest()
|
||||
|
||||
# Take the first 12 characters of the hash and format as a MAC address
|
||||
mac_hash = ':'.join(sha256_hash[i:i + 2] for i in range(0, 12, 2))
|
||||
|
||||
return mac_hash
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# BEGIN
|
||||
# ===============================================================================
|
||||
|
||||
@@ -13,9 +13,6 @@ The plugin connects to your Pi-hole’s API and retrieves:
|
||||
|
||||
NetAlertX then uses this information to match or create devices in your system.
|
||||
|
||||
> [!TIP]
|
||||
> Some tip.
|
||||
|
||||
### Quick setup guide
|
||||
|
||||
* You are running **Pi-hole v6** or newer.
|
||||
@@ -30,21 +27,13 @@ No additional Pi-hole configuration is required.
|
||||
|
||||
| Setting Key | Description |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------- |
|
||||
| **PIHOLEAPI_URL** | Your Pi-hole base URL. |
|
||||
| **PIHOLEAPI_URL** | Your Pi-hole base URL. |
|
||||
| **PIHOLEAPI_PASSWORD** | The Web UI base64 encoded (en-/decoding handled by the app) admin password. |
|
||||
| **PIHOLEAPI_SSL_VERIFY** | Whether to verify HTTPS certificates. Disable only for self-signed certificates. |
|
||||
| **PIHOLEAPI_RUN_TIMEOUT** | Request timeout in seconds. |
|
||||
| **PIHOLEAPI_API_MAXCLIENTS** | Maximum number of devices to request from Pi-hole. Defaults are usually fine. |
|
||||
| **PIHOLEAPI_FAKE_MAC** | Generate FAKE MAC from IP. |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
| Setting Key | Sample Value |
|
||||
| ---------------------------- | -------------------------------------------------- |
|
||||
| **PIHOLEAPI_URL** | `http://pi.hole/` |
|
||||
| **PIHOLEAPI_PASSWORD** | `passw0rd` |
|
||||
| **PIHOLEAPI_SSL_VERIFY** | `true` |
|
||||
| **PIHOLEAPI_RUN_TIMEOUT** | `30` |
|
||||
| **PIHOLEAPI_API_MAXCLIENTS** | `500` |
|
||||
|
||||
### ⚠️ Troubleshooting
|
||||
|
||||
@@ -110,6 +99,32 @@ Then re-run the plugin.
|
||||
|
||||
---
|
||||
|
||||
#### ❌ Some devices are missing
|
||||
|
||||
Check:
|
||||
|
||||
* Pi-hole shows devices under **Settings → Network**
|
||||
* NetAlertX logs contain:
|
||||
|
||||
```
|
||||
[PIHOLEAPI] Skipping invalid MAC (see PIHOLEAPI_FAKE_MAC setting) ...
|
||||
```
|
||||
|
||||
If devices are missing:
|
||||
|
||||
* The app skipps devices with invalid MACs
|
||||
* Enable PIHOLEAPI_FAKE_MAC if you want to import these devices with a fake mac and you are not concerned with data inconsistencies later on
|
||||
|
||||
Try enabling PIHOLEAPI_FAKE_MAC:
|
||||
|
||||
```
|
||||
PIHOLEAPI_FAKE_MAC = 1
|
||||
```
|
||||
|
||||
Then re-run the plugin.
|
||||
|
||||
---
|
||||
|
||||
#### ❌ Wrong or missing hostnames
|
||||
|
||||
Pi-hole only reports names it knows from:
|
||||
@@ -122,7 +137,7 @@ If names are missing, confirm they appear in Pi-hole’s own UI first.
|
||||
|
||||
### Notes
|
||||
|
||||
- Additional notes, limitations, Author info.
|
||||
- Additional notes, limitations, Author info.
|
||||
|
||||
- Version: 1.0.0
|
||||
- Author: `jokob-sk`, `leiweibau`
|
||||
|
||||
@@ -279,6 +279,41 @@
|
||||
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "FAKE_MAC",
|
||||
"type": {
|
||||
"dataType": "boolean",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"type": "checkbox"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": false,
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Fake MAC if empty"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Some PiHole devices don't have a MAC assigned. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -23,6 +23,7 @@ from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from const import logPath # noqa: E402 [flake8 lint suppression]
|
||||
import conf # noqa: E402 [flake8 lint suppression]
|
||||
from pytz import timezone # noqa: E402 [flake8 lint suppression]
|
||||
from utils.crypto_utils import string_to_mac_hash # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
# Setup timezone & logger using standard NAX helpers
|
||||
conf.tz = timezone(get_setting_value('TIMEZONE'))
|
||||
@@ -42,6 +43,7 @@ PIHOLEAPI_SES_CSRF = None
|
||||
PIHOLEAPI_API_MAXCLIENTS = None
|
||||
PIHOLEAPI_VERIFY_SSL = True
|
||||
PIHOLEAPI_RUN_TIMEOUT = 10
|
||||
PIHOLEAPI_FAKE_MAC = get_setting_value('PIHOLEAPI_FAKE_MAC')
|
||||
VERSION_DATE = "NAX-PIHOLEAPI-1.0"
|
||||
|
||||
|
||||
@@ -222,8 +224,14 @@ def gather_device_entries():
|
||||
if ip in iplist:
|
||||
lastQuery = str(now_ts)
|
||||
|
||||
tmpMac = hwaddr.lower()
|
||||
|
||||
# ensure fake mac if enabled
|
||||
if PIHOLEAPI_FAKE_MAC and is_mac(tmpMac) is False:
|
||||
tmpMac = string_to_mac_hash(ip)
|
||||
|
||||
entries.append({
|
||||
'mac': hwaddr.lower(),
|
||||
'mac': tmpMac,
|
||||
'ip': ip,
|
||||
'name': name,
|
||||
'macVendor': macVendor,
|
||||
@@ -281,7 +289,7 @@ def main():
|
||||
foreignKey=str(entry['mac'])
|
||||
)
|
||||
else:
|
||||
mylog('verbose', [f"[{pluginName}] Skipping invalid MAC: {entry['name']}|{entry['mac']}|{entry['ip']}"])
|
||||
mylog('verbose', [f"[{pluginName}] Skipping invalid MAC (see PIHOLEAPI_FAKE_MAC setting): {entry['name']}|{entry['mac']}|{entry['ip']}"])
|
||||
|
||||
# Write result file for NetAlertX to ingest
|
||||
plugin_objects.write_result_file()
|
||||
|
||||
@@ -6,7 +6,7 @@ A plugin for importing devices from an SNMP-enabled router or switch. Using SNMP
|
||||
|
||||
Specify the following settings in the Settings section of NetAlertX:
|
||||
|
||||
- `SNMPDSC_routers` - A list of `snmpwalk` commands to execute against IP addresses of routers/switches with SNMP turned on. For example:
|
||||
- `SNMPDSC_routers` - A list of `snmpwalk` commands to execute against IP addresses of routers/switches with SNMP turned on. For example:
|
||||
|
||||
- `snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2`
|
||||
- `snmpwalk -v 2c -c public -Oxsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2` (note: lower case `x`)
|
||||
@@ -14,6 +14,14 @@ Specify the following settings in the Settings section of NetAlertX:
|
||||
|
||||
If unsure, please check [snmpwalk examples](https://www.comparitech.com/net-admin/snmpwalk-examples-windows-linux/).
|
||||
|
||||
Supported output formats:
|
||||
|
||||
```
|
||||
ipNetToMediaPhysAddress[3][192.168.1.9] 6C:6C:6C:6C:6C:b6C1
|
||||
IP-MIB::ipNetToMediaPhysAddress.17.10.10.3.202 = STRING: f8:81:1a:ef:ef:ef
|
||||
mib-2.3.1.1.2.15.1.192.168.1.14 "2C F4 32 18 61 43 "
|
||||
```
|
||||
|
||||
### Setup Cisco IOS
|
||||
|
||||
Enable IOS SNMP service and restrict to selected (internal) IP/Subnet.
|
||||
|
||||
@@ -30,7 +30,7 @@ RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
|
||||
|
||||
|
||||
def main():
|
||||
mylog('verbose', ['[SNMPDSC] In script '])
|
||||
mylog('verbose', f"[{pluginName}] In script ")
|
||||
|
||||
# init global variables
|
||||
global snmpWalkCmds
|
||||
@@ -57,7 +57,7 @@ def main():
|
||||
commands = [snmpWalkCmds]
|
||||
|
||||
for cmd in commands:
|
||||
mylog('verbose', ['[SNMPDSC] Router snmpwalk command: ', cmd])
|
||||
mylog('verbose', [f"[{pluginName}] Router snmpwalk command: ", cmd])
|
||||
# split the string, remove white spaces around each item, and exclude any empty strings
|
||||
snmpwalkArgs = [arg.strip() for arg in cmd.split(' ') if arg.strip()]
|
||||
|
||||
@@ -72,7 +72,7 @@ def main():
|
||||
timeout=(timeoutSetting)
|
||||
)
|
||||
|
||||
mylog('verbose', ['[SNMPDSC] output: ', output])
|
||||
mylog('verbose', [f"[{pluginName}] output: ", output])
|
||||
|
||||
lines = output.split('\n')
|
||||
|
||||
@@ -80,6 +80,8 @@ def main():
|
||||
|
||||
tmpSplt = line.split('"')
|
||||
|
||||
# Expected Format:
|
||||
# mib-2.3.1.1.2.15.1.192.168.1.14 "2C F4 32 18 61 43 "
|
||||
if len(tmpSplt) == 3:
|
||||
|
||||
ipStr = tmpSplt[0].split('.')[-4:] # Get the last 4 elements to extract the IP
|
||||
@@ -89,7 +91,7 @@ def main():
|
||||
macAddress = ':'.join(macStr)
|
||||
ipAddress = '.'.join(ipStr)
|
||||
|
||||
mylog('verbose', [f'[SNMPDSC] IP: {ipAddress} MAC: {macAddress}'])
|
||||
mylog('verbose', [f"[{pluginName}] IP: {ipAddress} MAC: {macAddress}"])
|
||||
|
||||
plugin_objects.add_object(
|
||||
primaryId = handleEmpty(macAddress),
|
||||
@@ -100,8 +102,40 @@ def main():
|
||||
foreignKey = handleEmpty(macAddress) # Use the primary ID as the foreign key
|
||||
)
|
||||
else:
|
||||
mylog('verbose', ['[SNMPDSC] ipStr does not seem to contain a valid IP:', ipStr])
|
||||
mylog('verbose', [f"[{pluginName}] ipStr does not seem to contain a valid IP:", ipStr])
|
||||
|
||||
# Expected Format:
|
||||
# IP-MIB::ipNetToMediaPhysAddress.17.10.10.3.202 = STRING: f8:81:1a:ef:ef:ef
|
||||
elif "ipNetToMediaPhysAddress" in line and "=" in line and "STRING:" in line:
|
||||
|
||||
# Split on "=" → ["IP-MIB::ipNetToMediaPhysAddress.xxx.xxx.xxx.xxx ", " STRING: aa:bb:cc:dd:ee:ff"]
|
||||
left, right = line.split("=", 1)
|
||||
|
||||
# Extract the MAC (right side)
|
||||
macAddress = right.split("STRING:")[-1].strip()
|
||||
macAddress = normalize_mac(macAddress)
|
||||
|
||||
# Extract IP address from the left side
|
||||
# tail of the OID: last 4 integers = IPv4 address
|
||||
oid_parts = left.strip().split('.')
|
||||
ip_parts = oid_parts[-4:]
|
||||
ipAddress = ".".join(ip_parts)
|
||||
|
||||
mylog('verbose', [f"[{pluginName}] (fallback) IP: {ipAddress} MAC: {macAddress}"])
|
||||
|
||||
plugin_objects.add_object(
|
||||
primaryId = handleEmpty(macAddress),
|
||||
secondaryId = handleEmpty(ipAddress),
|
||||
watched1 = '(unknown)',
|
||||
watched2 = handleEmpty(snmpwalkArgs[6]),
|
||||
extra = handleEmpty(line),
|
||||
foreignKey = handleEmpty(macAddress)
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
# Expected Format:
|
||||
# ipNetToMediaPhysAddress[3][192.168.1.9] 6C:6C:6C:6C:6C:b6C1
|
||||
elif line.startswith('ipNetToMediaPhysAddress'):
|
||||
# Format: snmpwalk -OXsq output
|
||||
parts = line.split()
|
||||
@@ -110,7 +144,7 @@ def main():
|
||||
ipAddress = parts[0].split('[')[-1][:-1]
|
||||
macAddress = normalize_mac(parts[1])
|
||||
|
||||
mylog('verbose', [f'[SNMPDSC] IP: {ipAddress} MAC: {macAddress}'])
|
||||
mylog('verbose', [f"[{pluginName}] IP: {ipAddress} MAC: {macAddress}"])
|
||||
|
||||
plugin_objects.add_object(
|
||||
primaryId = handleEmpty(macAddress),
|
||||
@@ -121,7 +155,7 @@ def main():
|
||||
foreignKey = handleEmpty(macAddress)
|
||||
)
|
||||
|
||||
mylog('verbose', ['[SNMPDSC] Entries found: ', len(plugin_objects)])
|
||||
mylog('verbose', [f"[{pluginName}] Entries found: ", len(plugin_objects)])
|
||||
|
||||
plugin_objects.write_result_file()
|
||||
|
||||
|
||||
0
install/production-filesystem/entrypoint.d/0-storage-permission.sh
Executable file → Normal file
@@ -1,32 +1,57 @@
|
||||
#!/bin/sh
|
||||
# This script checks if the database file exists, and if not, creates it with the initial schema.
|
||||
# It is intended to be run at the first start of the application.
|
||||
# Ensures the database exists, or creates a new one on first run.
|
||||
# Intended to run only at initial startup.
|
||||
|
||||
# If ALWAYS_FRESH_INSTALL is true, remove the database to force a rebuild.
|
||||
if [ "${ALWAYS_FRESH_INSTALL}" = "true" ]; then
|
||||
if [ -f "${NETALERTX_DB_FILE}" ]; then
|
||||
# Provide feedback to the user.
|
||||
>&2 echo "INFO: ALWAYS_FRESH_INSTALL is true. Removing existing database to force a fresh installation."
|
||||
rm -f "${NETALERTX_DB_FILE}" "${NETALERTX_DB_FILE}-shm" "${NETALERTX_DB_FILE}-wal"
|
||||
set -eu
|
||||
|
||||
YELLOW=$(printf '\033[1;33m')
|
||||
CYAN=$(printf '\033[1;36m')
|
||||
RED=$(printf '\033[1;31m')
|
||||
RESET=$(printf '\033[0m')
|
||||
|
||||
# Ensure DB folder exists
|
||||
if [ ! -d "${NETALERTX_DB}" ]; then
|
||||
if ! mkdir -p "${NETALERTX_DB}"; then
|
||||
>&2 printf "%s" "${RED}"
|
||||
>&2 cat <<EOF
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
❌ Error creating DB folder in: ${NETALERTX_DB}
|
||||
|
||||
A database directory is required for proper operation, however there appear to be
|
||||
insufficient permissions on this mount or it is otherwise inaccessible.
|
||||
|
||||
More info: https://github.com/jokob-sk/NetAlertX/blob/main/docs/FILE_PERMISSIONS.md
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
EOF
|
||||
>&2 printf "%s" "${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
# Otherwise, if the db exists, exit.
|
||||
elif [ -f "${NETALERTX_DB_FILE}" ]; then
|
||||
chmod 700 "${NETALERTX_DB}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Fresh rebuild requested
|
||||
if [ "${ALWAYS_FRESH_INSTALL:-false}" = "true" ] && [ -f "${NETALERTX_DB_FILE}" ]; then
|
||||
>&2 echo "INFO: ALWAYS_FRESH_INSTALL enabled — removing existing database."
|
||||
rm -f "${NETALERTX_DB_FILE}" "${NETALERTX_DB_FILE}-shm" "${NETALERTX_DB_FILE}-wal"
|
||||
fi
|
||||
|
||||
# If file exists now, nothing to do
|
||||
if [ -f "${NETALERTX_DB_FILE}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CYAN=$(printf '\033[1;36m')
|
||||
RESET=$(printf '\033[0m')
|
||||
>&2 printf "%s" "${CYAN}"
|
||||
>&2 cat <<EOF
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
🆕 First run detected. Building initial database schema in ${NETALERTX_DB_FILE}.
|
||||
🆕 First run detected — building initial database at: ${NETALERTX_DB_FILE}
|
||||
|
||||
Do not interrupt this step. Once complete, consider backing up the fresh
|
||||
database before onboarding sensitive networks.
|
||||
Do not interrupt this step. When complete, consider backing up the fresh
|
||||
DB before onboarding sensitive or critical networks.
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
EOF
|
||||
>&2 printf "%s" "${RESET}"
|
||||
|
||||
|
||||
# Write all text to db file until we see "end-of-database-schema"
|
||||
sqlite3 "${NETALERTX_DB_FILE}" <<'end-of-database-schema'
|
||||
CREATE TABLE Events (eve_MAC STRING (50) NOT NULL COLLATE NOCASE, eve_IP STRING (50) NOT NULL COLLATE NOCASE, eve_DateTime DATETIME NOT NULL, eve_EventType STRING (30) NOT NULL COLLATE NOCASE, eve_AdditionalInfo STRING (250) DEFAULT (''), eve_PendingAlertEmail BOOLEAN NOT NULL CHECK (eve_PendingAlertEmail IN (0, 1)) DEFAULT (1), eve_PairEventRowid INTEGER);
|
||||
@@ -91,7 +116,7 @@ CREATE TABLE IF NOT EXISTS "Parameters" (
|
||||
);
|
||||
CREATE TABLE Plugins_Objects(
|
||||
"Index" INTEGER,
|
||||
Plugin TEXT NOT NULL,
|
||||
Plugin TEXT NOT NULL,
|
||||
Object_PrimaryID TEXT NOT NULL,
|
||||
Object_SecondaryID TEXT NOT NULL,
|
||||
DateTimeCreated TEXT NOT NULL,
|
||||
@@ -164,7 +189,7 @@ CREATE TABLE Plugins_Language_Strings(
|
||||
Extra TEXT NOT NULL,
|
||||
PRIMARY KEY("Index" AUTOINCREMENT)
|
||||
);
|
||||
CREATE TABLE CurrentScan (
|
||||
CREATE TABLE CurrentScan (
|
||||
cur_MAC STRING(50) NOT NULL COLLATE NOCASE,
|
||||
cur_IP STRING(50) NOT NULL COLLATE NOCASE,
|
||||
cur_Vendor STRING(250),
|
||||
@@ -191,11 +216,11 @@ CREATE TABLE IF NOT EXISTS "AppEvents" (
|
||||
"ObjectPrimaryID" TEXT,
|
||||
"ObjectSecondaryID" TEXT,
|
||||
"ObjectForeignKey" TEXT,
|
||||
"ObjectIndex" TEXT,
|
||||
"ObjectIsNew" BOOLEAN,
|
||||
"ObjectIsArchived" BOOLEAN,
|
||||
"ObjectIndex" TEXT,
|
||||
"ObjectIsNew" BOOLEAN,
|
||||
"ObjectIsArchived" BOOLEAN,
|
||||
"ObjectStatusColumn" TEXT,
|
||||
"ObjectStatus" TEXT,
|
||||
"ObjectStatus" TEXT,
|
||||
"AppEventType" TEXT,
|
||||
"Helper1" TEXT,
|
||||
"Helper2" TEXT,
|
||||
@@ -233,21 +258,21 @@ CREATE INDEX IDX_dev_Favorite ON Devices (devFavorite);
|
||||
CREATE INDEX IDX_dev_LastIP ON Devices (devLastIP);
|
||||
CREATE INDEX IDX_dev_NewDevice ON Devices (devIsNew);
|
||||
CREATE INDEX IDX_dev_Archived ON Devices (devIsArchived);
|
||||
CREATE VIEW Events_Devices AS
|
||||
SELECT *
|
||||
FROM Events
|
||||
CREATE VIEW Events_Devices AS
|
||||
SELECT *
|
||||
FROM Events
|
||||
LEFT JOIN Devices ON eve_MAC = devMac
|
||||
/* Events_Devices(eve_MAC,eve_IP,eve_DateTime,eve_EventType,eve_AdditionalInfo,eve_PendingAlertEmail,eve_PairEventRowid,devMac,devName,devOwner,devType,devVendor,devFavorite,devGroup,devComments,devFirstConnection,devLastConnection,devLastIP,devStaticIP,devScan,devLogEvents,devAlertEvents,devAlertDown,devSkipRepeated,devLastNotification,devPresentLastScan,devIsNew,devLocation,devIsArchived,devParentMAC,devParentPort,devIcon,devGUID,devSite,devSSID,devSyncHubNode,devSourcePlugin,devCustomProps) */;
|
||||
CREATE VIEW LatestEventsPerMAC AS
|
||||
WITH RankedEvents AS (
|
||||
SELECT
|
||||
SELECT
|
||||
e.*,
|
||||
ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num
|
||||
FROM Events AS e
|
||||
)
|
||||
SELECT
|
||||
e.*,
|
||||
d.*,
|
||||
SELECT
|
||||
e.*,
|
||||
d.*,
|
||||
c.*
|
||||
FROM RankedEvents AS e
|
||||
LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac
|
||||
@@ -286,11 +311,11 @@ CREATE VIEW Convert_Events_to_Sessions AS SELECT EVE1.eve_MAC,
|
||||
CREATE TRIGGER "trg_insert_devices"
|
||||
AFTER INSERT ON "Devices"
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM AppEvents
|
||||
WHERE AppEventProcessed = 0
|
||||
SELECT 1 FROM AppEvents
|
||||
WHERE AppEventProcessed = 0
|
||||
AND ObjectType = 'Devices'
|
||||
AND ObjectGUID = NEW.devGUID
|
||||
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
|
||||
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
|
||||
AND AppEventType = 'insert'
|
||||
)
|
||||
BEGIN
|
||||
@@ -311,18 +336,18 @@ CREATE TRIGGER "trg_insert_devices"
|
||||
"AppEventType"
|
||||
)
|
||||
VALUES (
|
||||
|
||||
|
||||
lower(
|
||||
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
|
||||
substr(hex( randomblob(2)), 2) || '-' ||
|
||||
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
|
||||
substr(hex( randomblob(2)), 2) || '-' ||
|
||||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
|
||||
substr(hex(randomblob(2)), 2) || '-' ||
|
||||
substr(hex(randomblob(2)), 2) || '-' ||
|
||||
hex(randomblob(6))
|
||||
)
|
||||
,
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
,
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
NEW.devGUID, -- ObjectGUID
|
||||
NEW.devMac, -- ObjectPrimaryID
|
||||
NEW.devLastIP, -- ObjectSecondaryID
|
||||
@@ -338,11 +363,11 @@ CREATE TRIGGER "trg_insert_devices"
|
||||
CREATE TRIGGER "trg_update_devices"
|
||||
AFTER UPDATE ON "Devices"
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM AppEvents
|
||||
WHERE AppEventProcessed = 0
|
||||
SELECT 1 FROM AppEvents
|
||||
WHERE AppEventProcessed = 0
|
||||
AND ObjectType = 'Devices'
|
||||
AND ObjectGUID = NEW.devGUID
|
||||
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
|
||||
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
|
||||
AND AppEventType = 'update'
|
||||
)
|
||||
BEGIN
|
||||
@@ -363,18 +388,18 @@ CREATE TRIGGER "trg_update_devices"
|
||||
"AppEventType"
|
||||
)
|
||||
VALUES (
|
||||
|
||||
|
||||
lower(
|
||||
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
|
||||
substr(hex( randomblob(2)), 2) || '-' ||
|
||||
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
|
||||
substr(hex( randomblob(2)), 2) || '-' ||
|
||||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
|
||||
substr(hex(randomblob(2)), 2) || '-' ||
|
||||
substr(hex(randomblob(2)), 2) || '-' ||
|
||||
hex(randomblob(6))
|
||||
)
|
||||
,
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
,
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
NEW.devGUID, -- ObjectGUID
|
||||
NEW.devMac, -- ObjectPrimaryID
|
||||
NEW.devLastIP, -- ObjectSecondaryID
|
||||
@@ -390,11 +415,11 @@ CREATE TRIGGER "trg_update_devices"
|
||||
CREATE TRIGGER "trg_delete_devices"
|
||||
AFTER DELETE ON "Devices"
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM AppEvents
|
||||
WHERE AppEventProcessed = 0
|
||||
SELECT 1 FROM AppEvents
|
||||
WHERE AppEventProcessed = 0
|
||||
AND ObjectType = 'Devices'
|
||||
AND ObjectGUID = OLD.devGUID
|
||||
AND ObjectStatus = CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
|
||||
AND ObjectStatus = CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
|
||||
AND AppEventType = 'delete'
|
||||
)
|
||||
BEGIN
|
||||
@@ -415,18 +440,18 @@ CREATE TRIGGER "trg_delete_devices"
|
||||
"AppEventType"
|
||||
)
|
||||
VALUES (
|
||||
|
||||
|
||||
lower(
|
||||
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
|
||||
substr(hex( randomblob(2)), 2) || '-' ||
|
||||
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
|
||||
substr(hex( randomblob(2)), 2) || '-' ||
|
||||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
|
||||
substr(hex(randomblob(2)), 2) || '-' ||
|
||||
substr(hex(randomblob(2)), 2) || '-' ||
|
||||
hex(randomblob(6))
|
||||
)
|
||||
,
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
,
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
OLD.devGUID, -- ObjectGUID
|
||||
OLD.devMac, -- ObjectPrimaryID
|
||||
OLD.devLastIP, -- ObjectSecondaryID
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
# override-config.sh - Handles APP_CONF_OVERRIDE environment variable
|
||||
|
||||
OVERRIDE_FILE="${NETALERTX_CONFIG}/app_conf_override.json"
|
||||
|
||||
# Ensure config directory exists
|
||||
mkdir -p "$(dirname "$NETALERTX_CONFIG")" || {
|
||||
>&2 echo "ERROR: Failed to create config directory $(dirname "$NETALERTX_CONFIG")"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Remove old override file if it exists
|
||||
rm -f "$OVERRIDE_FILE"
|
||||
|
||||
# Check if APP_CONF_OVERRIDE is set
|
||||
if [ -z "$APP_CONF_OVERRIDE" ]; then
|
||||
>&2 echo "APP_CONF_OVERRIDE is not set. Skipping override config file creation."
|
||||
else
|
||||
# Save the APP_CONF_OVERRIDE env variable as a JSON file
|
||||
echo "$APP_CONF_OVERRIDE" > "$OVERRIDE_FILE" || {
|
||||
>&2 echo "ERROR: Failed to write override config to $OVERRIDE_FILE"
|
||||
exit 2
|
||||
}
|
||||
|
||||
RESET=$(printf '\033[0m')
|
||||
>&2 cat <<EOF
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
📝 APP_CONF_OVERRIDE detected. Configuration written to $OVERRIDE_FILE.
|
||||
|
||||
Make sure the JSON content is correct before starting the application.
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
EOF
|
||||
|
||||
>&2 printf "%s" "${RESET}"
|
||||
fi
|
||||
@@ -14,7 +14,7 @@ if ! awk '$2 == "/" && $4 ~ /ro/ {found=1} END {exit !found}' /proc/mounts; then
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
⚠️ 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
|
||||
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
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
|
||||
# Define ports from ENV variables, applying defaults
|
||||
PORT_APP=${PORT:-20211}
|
||||
PORT_GQL=${APP_CONF_OVERRIDE:-${GRAPHQL_PORT:-20212}}
|
||||
# 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}.
|
||||
# # 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.
|
||||
# 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
|
||||
# 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
|
||||
@@ -53,17 +53,17 @@ if echo "$LISTENING_PORTS" | grep -q ":${PORT_APP}$"; then
|
||||
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.
|
||||
# # 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.
|
||||
# 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
|
||||
# https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# EOF
|
||||
# fi
|
||||
@@ -87,7 +87,8 @@ CORS(
|
||||
r"/dbquery/*": {"origins": "*"},
|
||||
r"/messaging/*": {"origins": "*"},
|
||||
r"/events/*": {"origins": "*"},
|
||||
r"/logs/*": {"origins": "*"}
|
||||
r"/logs/*": {"origins": "*"},
|
||||
r"/auth/*": {"origins": "*"}
|
||||
},
|
||||
supports_credentials=True,
|
||||
allow_headers=["Authorization", "Content-Type"],
|
||||
@@ -744,6 +745,23 @@ def sync_endpoint():
|
||||
return jsonify({"success": False, "message": "ERROR: No allowed", "error": "Method Not Allowed"}), 405
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Auth endpoint
|
||||
# --------------------------
|
||||
@app.route("/auth", methods=["GET"])
|
||||
def check_auth():
|
||||
if not is_authorized():
|
||||
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
|
||||
|
||||
elif request.method == "GET":
|
||||
return jsonify({"success": True, "message": "Authentication check successful"}), 200
|
||||
else:
|
||||
msg = "[sync endpoint] Method Not Allowed"
|
||||
write_notification(msg, "alert")
|
||||
mylog("verbose", [msg])
|
||||
return jsonify({"success": False, "message": "ERROR: No allowed", "error": "Method Not Allowed"}), 405
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Background Server Start
|
||||
# --------------------------
|
||||
|
||||
@@ -70,3 +70,13 @@ def generate_deterministic_guid(plugin, primary_id, secondary_id):
|
||||
"""Generates a deterministic GUID based on plugin, primary ID, and secondary ID."""
|
||||
data = f"{plugin}-{primary_id}-{secondary_id}".encode("utf-8")
|
||||
return str(uuid.UUID(hashlib.md5(data).hexdigest()))
|
||||
|
||||
|
||||
def string_to_mac_hash(input_string):
|
||||
# Calculate a hash using SHA-256
|
||||
sha256_hash = hashlib.sha256(input_string.encode()).hexdigest()
|
||||
|
||||
# Take the first 12 characters of the hash and format as a MAC address
|
||||
mac_hash = ':'.join(sha256_hash[i:i + 2] for i in range(0, 12, 2))
|
||||
|
||||
return mac_hash
|
||||
|
||||
66
test/api_endpoints/test_auth_endpoints.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# tests/test_auth.py
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value # noqa: E402
|
||||
from api_server.api_server_start import app # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_token():
|
||||
"""Load API token from system settings (same as other tests)."""
|
||||
return get_setting_value("API_TOKEN")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Flask test client."""
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
# -------------------------
|
||||
# AUTH ENDPOINT TESTS
|
||||
# -------------------------
|
||||
|
||||
def test_auth_ok(client, api_token):
|
||||
"""Valid token should allow access."""
|
||||
resp = client.get("/auth", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
|
||||
data = resp.get_json()
|
||||
assert data is not None
|
||||
assert data.get("success") is True
|
||||
assert "successful" in data.get("message", "").lower()
|
||||
|
||||
|
||||
def test_auth_missing_token(client):
|
||||
"""Missing token should be forbidden."""
|
||||
resp = client.get("/auth")
|
||||
assert resp.status_code == 403
|
||||
|
||||
data = resp.get_json()
|
||||
assert data is not None
|
||||
assert data.get("success") is False
|
||||
assert "not authorized" in data.get("message", "").lower()
|
||||
|
||||
|
||||
def test_auth_invalid_token(client):
|
||||
"""Invalid bearer token should be forbidden."""
|
||||
resp = client.get("/auth", headers=auth_headers("INVALID-TOKEN"))
|
||||
assert resp.status_code == 403
|
||||
|
||||
data = resp.get_json()
|
||||
assert data is not None
|
||||
assert data.get("success") is False
|
||||
assert "not authorized" in data.get("message", "").lower()
|
||||