From 9b0f45b88ba885f55d70a694be423bfcf4f7671b Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Mon, 27 Oct 2025 14:21:17 +1100 Subject: [PATCH 01/28] DOCS: migration prep Signed-off-by: jokob-sk --- docs/MIGRATION.md | 167 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 28 deletions(-) diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index e3aebcce..33dc60c6 100755 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,27 +1,47 @@ -# Migration form PiAlert to NetAlertX +# Migration -> [!WARNING] -> Follow this guide only after you you downloaded and started a version of NetAlertX prior to v25.6.7 (e.g. `docker pull ghcr.io/jokob-sk/netalertx:25.5.24`) at least once after previously using the PiAlert image. Later versions don't support migration and devices and settings will have to migrated manually, e.g. via [CSV import](./DEVICES_BULK_EDITING.md). +If upgrading from older versions of NetAlertX (or PiAlert (by jokob-sk)) the following data and setup migration steps need to be followed. -## STEPS: +> [!TIP] +> It's always important to have a [backup strategy](./BACKUPS.md) in place. -> [!TIP] -> In short: The application will auto-migrate the database, config, and all device information. A ticker message on top will be displayed until you update your docker mount points. It's always good to have a [backup strategy](./BACKUPS.md) in place. +## Migration scenarios -1. Backup your current config and database (optional `devices.csv` to have a backup) (See bellow tip if facing issues) -2. Stop the container -2. Update the Docker file mount locations in your `docker-compose.yml` or docker run command (See bellow **New Docker mount locations**). -3. Rename the DB and conf files to `app.db` and `app.conf` and place them in the appropriate location. -4. Start the Container +- You are running PiAlert (by jokob-sk) + → [Read the 1.1 Migration from PiAlert to NetAlertX `v25.5.24`](#11-migration-from-pialert-to-netalertx-v25524) + +- You are running NetAlertX (by jokob-sk) `25.5.24` or older + → [Read the 1.2 Migration from NetAlertX `v25.5.24`](#12-migration-from-netalertx-v25524) + +- You are running NetAlertX (by jokob-sk) (`v25.6.7` to `v25.10.1`) + → [Read the 1.3 Migration from NetAlertX `v25.10.1`](#13-migration-from-netalertx-v25101) + + +### 1.0 Manual Migration + +You can migrate data manually, for example by exporting and importing devices using the [CSV import](./DEVICES_BULK_EDITING.md) method. + + +### 1.1 Migration from PiAlert to NetAlertX `v25.5.24` + +#### STEPS: + +The application will automatically migrate the database, configuration, and all device information. +A ticker message will appear at the top of the web UI until you update your Docker mount points. + +1. Stop the container +2. [Back up your setup](./BACKUPS.md) +3. Update the Docker file mount locations in your `docker-compose.yml` or docker run command (See below **New Docker mount locations**). +4. Rename the DB and conf files to `app.db` and `app.conf` and place them in the appropriate location. +5. Start the container > [!TIP] -> If you have troubles accessing past backups, config or database files you can copy them into the newly mapped directories, for example by running this command in the container: `cp -r /app/config /home/pi/pialert/config/old_backup_files`. This should create a folder in the `config` directory called `old_backup_files` conatining all the files in that location. Another approach is to map the old location and the new one at the same time to copy things over. +> If you have troubles accessing past backups, config or database files you can copy them into the newly mapped directories, for example by running this command in the container: `cp -r /app/config /home/pi/pialert/config/old_backup_files`. This should create a folder in the `config` directory called `old_backup_files` containing all the files in that location. Another approach is to map the old location and the new one at the same time to copy things over. +#### New Docker mount locations -### New Docker mount locations - -The application installation folder in the docker container has changed from `/home/pi/pialert` to `/app`. That means the new mount points are: +The internal application path in the container has changed from `/home/pi/pialert` to `/app`. Update your volume mounts as follows: | Old mount point | New mount point | |----------------------|---------------| @@ -41,13 +61,13 @@ The application installation folder in the docker container has changed from `/h > The application uses symlinks linking the old db and config locations to the new ones, so data loss should not occur. [Backup strategies](./BACKUPS.md) are still recommended to backup your setup. -# Examples +#### Examples Examples of docker files with the new mount points. -## Example 1: Mapping folders +##### Example 1: Mapping folders -### Old docker-compose.yml +###### Old docker-compose.yml ```yaml services: @@ -68,15 +88,13 @@ services: - PORT=20211 ``` -### New docker-compose.yml +###### New docker-compose.yml ```yaml services: netalertx: # ⚠ This has changed (🟡optional) container_name: netalertx # ⚠ This has changed (🟡optional) - # use the below line if you want to test the latest dev image - # image: "ghcr.io/jokob-sk/netalertx-dev:latest" - image: "ghcr.io/jokob-sk/netalertx:latest" # ⚠ This has changed (🟡optional/🔺required in future) + image: "ghcr.io/jokob-sk/netalertx:25.5.24" # ⚠ This has changed (🟡optional/🔺required in future) network_mode: "host" restart: unless-stopped volumes: @@ -90,12 +108,12 @@ services: ``` -## Example 2: Mapping files +##### Example 2: Mapping files > [!NOTE] > The recommendation is to map folders as in Example 1, map files directly only when needed. -### Old docker-compose.yml +###### Old docker-compose.yml ```yaml services: @@ -116,15 +134,13 @@ services: - PORT=20211 ``` -### New docker-compose.yml +###### New docker-compose.yml ```yaml services: netalertx: # ⚠ This has changed (🟡optional) container_name: netalertx # ⚠ This has changed (🟡optional) - # use the below line if you want to test the latest dev image - # image: "ghcr.io/jokob-sk/netalertx-dev:latest" - image: "ghcr.io/jokob-sk/netalertx:latest" # ⚠ This has changed (🟡optional/🔺required in future) + image: "ghcr.io/jokob-sk/netalertx:25.5.24" # ⚠ This has changed (🟡optional/🔺required in future) network_mode: "host" restart: unless-stopped volumes: @@ -136,3 +152,98 @@ services: - TZ=Europe/Berlin - PORT=20211 ``` + + +### 1.2 Migration from NetAlertX `v25.5.24` + +Versions prior to `v25.10.1` require an intermediate migration through `v25.5.24` to ensure database compatibility. + +#### STEPS: + +1. Stop the container +2. [Back up your setup](./BACKUPS.md) +3. Upgrade to `v25.5.24` by pinning the release version (See Examples below) +4. Start the container and verify everything works as expected. +5. Stop the container +6. Upgrade to `v25.10.1` by pinning the release version (See Examples below) +7. Start the container and verify everything works as expected. + +#### Examples + +Examples of docker files with the tagged version. + +##### Example 1: Mapping folders + +###### docker-compose.yml changes + +```yaml +services: + netalertx: + container_name: netalertx + image: "ghcr.io/jokob-sk/netalertx:25.5.24" # ⚠ This is important (🔺required) + network_mode: "host" + restart: unless-stopped + volumes: + - local/path/config:/app/config + - local/path/db:/app/db + # (optional) useful for debugging if you have issues setting up the container + - local/path/logs:/app/log + environment: + - TZ=Europe/Berlin + - PORT=20211 +``` + +```yaml +services: + netalertx: + container_name: netalertx + image: "ghcr.io/jokob-sk/netalertx:25.10.1" # ⚠ This is important (🔺required) + network_mode: "host" + restart: unless-stopped + volumes: + - local/path/config:/app/config + - local/path/db:/app/db + # (optional) useful for debugging if you have issues setting up the container + - local/path/logs:/app/log + environment: + - TZ=Europe/Berlin + - PORT=20211 +``` + +### 1.3 Migration from NetAlertX `v25.10.1` + +> [!WARNING] +> This section is under development. The migration path from `v25.10.1` to future versions (e.g., `v25.11.x` and newer) will be published soon. + +#### STEPS: + +1. Stop the container +2. [Back up your setup](./BACKUPS.md) +3. Upgrade to `v25.10.1` by pinning the release version (See Examples below) +4. Start the container and verify everything works as expected. +5. Stop the container +6. 🔻 TBC 🔺 + +##### Example 1: Mapping folders + +###### docker-compose.yml changes + +```yaml +services: + netalertx: + container_name: netalertx + image: "ghcr.io/jokob-sk/netalertx:25.10.1" # ⚠ This is important (🔺required) + network_mode: "host" + restart: unless-stopped + volumes: + - local/path/config:/app/config + - local/path/db:/app/db + # (optional) useful for debugging if you have issues setting up the container + - local/path/logs:/app/log + environment: + - TZ=Europe/Berlin + - PORT=20211 +``` + + +🔻 TBC 🔺 \ No newline at end of file From 4d148f35cedac3f71d760e0058d44777e6b860f0 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Mon, 27 Oct 2025 03:33:50 +0000 Subject: [PATCH 02/28] DOCS: wording --- docs/MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 33dc60c6..4d7870c6 100755 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -156,7 +156,7 @@ services: ### 1.2 Migration from NetAlertX `v25.5.24` -Versions prior to `v25.10.1` require an intermediate migration through `v25.5.24` to ensure database compatibility. +Versions before `v25.10.1` require an intermediate migration through `v25.5.24` to ensure database compatibility. #### STEPS: From 6afa52e604b6c9aeb1855b2302e15bbb8739eb7a Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Tue, 28 Oct 2025 00:15:12 +0000 Subject: [PATCH 03/28] Security features overview --- docs/SECURITY_FEATURES.md | 85 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/SECURITY_FEATURES.md diff --git a/docs/SECURITY_FEATURES.md b/docs/SECURITY_FEATURES.md new file mode 100644 index 00000000..7fd5ae57 --- /dev/null +++ b/docs/SECURITY_FEATURES.md @@ -0,0 +1,85 @@ +# NetAlertX Security: A Layered Defense + +Your network security monitor has the "keys to the kingdom," making it a prime target for attackers. If it gets compromised, the game is over. + +NetAlertX is engineered from the ground up to prevent this. It's not just an app; it's a purpose-built **security appliance.** Its core design is built on a **zero-trust** philosophy, which is a modern way of saying we **assume a breach will happen** and plan for it. This isn't a single "lock on the door"; it's a **"defense-in-depth"** strategy, more like a medieval castle with a moat, high walls, and guards at every door. + +Here’s a breakdown of the defensive layers you get, right out of the box using the default configuration. + +### Feature 1: The "Digital Concrete" Filesystem + +**Methodology:** The core application and its system files are treated as immutable. Once built, the app's code is "set in concrete," preventing attackers from modifying it or planting malware. + +* **Immutable Filesystem:** At runtime, the container's entire filesystem is set to `read_only: true`. The application code, system libraries, and all other files are literally frozen. This single control neutralizes a massive range of common attacks. + +* **"Ownership-as-a-Lock" Pattern:** During the build, all system files are assigned to a special `readonly` user. This user has no login shell and no power to write to *any* files, even its own. It’s a clever, defense-in-depth locking mechanism. + +* **Data Segregation:** All user-specific data (like configurations and the device database) is stored completely outside the container in Docker volumes. The application is disposable; the data is persistent. + +**What's this mean to you:** Even if an attacker gets in, they **cannot modify the application code or plant malware.** It's like the app is set in digital concrete. + +### Feature 2: Surgical, "Keycard-Only" Access + +**Methodology:** The principle of least privilege is strictly enforced. Every process gets only the absolute minimum set of permissions needed for its specific job. + +* **Non-Privileged Execution:** The entire NetAlertX stack runs as a dedicated, low-power, non-root user (`netalertx`). No "god mode" privileges are available to the application. + +* **Kernel-Level Capability Revocation:** The container is launched with `cap_drop: - ALL`, which tells the Linux kernel to revoke *all* "root-like" special powers. + +* **Binary-Specific Privileges (setcap):** This is the "keycard" metaphor in action. After revoking all powers, the system uses `setcap` to grant specific, necessary permissions *only* to the binaries that absolutely require them (like `nmap` and `arp-scan`). This means that even if an attacker compromises the web server, they can't start scanning the network. The web server's "keycard" doesn't open the "scanning" door. + +**What's this mean to you:** A security breach is **firewalled.** An attacker who gets into the web UI **does not have the "keycard"** to start scanning your network or take over the system. The breach is contained. + +### Feature 3: Attack Surface "Amputation" + +**Methodology:** The potential attack surface is aggressively minimized by removing every non-essential tool an attacker would want to use. + +* **Package Manager Removal:** The `hardened` build stage explicitly deletes the Alpine package manager (`apk del apk-tools`). This makes it impossible for an attacker to simply `apk add` their malicious toolkit. + +* **`sudo` Neutralization:** All `sudo` configurations are removed, and the `/usr/bin/sudo` command is replaced with a non-functional shim. Any attempt to escalate privileges this way will fail. + +* **Build Toolchain Elimination:** The `Dockerfile` uses a multi-stage build. The initial "builder" stage, which contains all the powerful compilers (`gcc`) and development tools, is completely discarded. The final production image is lean and contains no build tools. + +* **Minimal User & Group Files:** The `hardened` stage scrubs the system's `passwd` and `group` files, removing all default system users to minimize potential avenues for privilege escalation. + +**What's this mean to you:** An attacker who breaks in finds themselves in an **empty room with no tools.** They have no `sudo` to get more power, no package manager to download weapons, and no compilers to build new ones. + +### Feature 4: "Self-Cleaning" Writable Areas + +**Methodology:** All writable locations are treated as untrusted, temporary, and non-executable by default. + +* **In-Memory Volatile Storage:** The `docker-compose.yml` configuration maps all temporary directories (e.g., `/app/log`, `/app/api`, `/tmp`) to in-memory `tmpfs` filesystems. They do not exist on the host's disk. + +* **Volatile Data:** Because these locations exist only in RAM, their contents are **instantly and irrevocably erased** when the container is stopped. This provides a "self-cleaning" mechanism that purges any attacker-dropped files or payloads on every single restart. + +* **Secure Mount Flags:** These in-memory mounts are configured with the `noexec` flag. This is a critical security control: it **prohibits the execution of any binary or script** from a location that is writable. + +**What's this mean to you:** Any malicious file an attacker *does* manage to drop is **written in invisible, non-permanent ink.** The file is written to RAM, not disk, so it **vaporizes the instant you restart** the container. Even worse for them, the `noexec` flag means they **can't even run the file** in the first place. + +### Feature 5: Built-in Resource Guardrails + +**Methodology:** The container is constrained by resource limits to function as a "good citizen" on the host system. This prevents a compromised or runaway process from consuming excessive resources, a common vector for Denial of Service (DoS) attacks. + +* **Process Limiting:** The `docker-compose.yml` defines a `pids_limit: 512`. This directly mitigates "fork bomb" attacks, where a process attempts to crash the host by recursively spawning thousands of new processes. + +* **Memory & CPU Limits:** The configuration file defines strict resource limits to prevent any single process from exhausting the host's available system resources. + +**What's this mean to you:** NetAlertX is a "good neighbor" and **can't be used to crash your host machine.** Even if a process is compromised, it's in a digital straitjacket and **cannot** pull a "denial of service" attack by hogging all your CPU or memory. + +### Feature 6: The "Pre-Flight" Self-Check + +**Methodology:** Before any services start, NetAlertX runs a comprehensive "pre-flight" check to ensure its own security and configuration are sound. It's like a built-in auditor that verifies its own defenses. + +* **Active Self-Diagnosis:** On every single boot, NetAlertX runs a series of startup pre-checks—and it's fast. The entire self-check process typically completes in less than a second, letting you get to the web UI in about three seconds from startup. + +* **Validates Its Own Security:** These checks actively inspect the other security features. For example, `check-0-permissions.sh` validates that all the "Digital Concrete" files are locked down and all the "Self-Cleaning" areas are writable, just as they should be. It also checks that the correct `netalertx` user is running the show, not `root`. + +* **Catches Misconfigurations:** This system acts as a "safety inspector" that catches misconfigurations *before* they can become security holes. If you've made a mistake in your configuration (like a bad folder permission or incorrect network mode), NetAlertX will tell you in the logs *why* it can't start, rather than just failing silently. + +**What's this mean to you:** The system is **self-aware and checks its own work.** You get instant feedback if a setting is wrong, and you get peace of mind on every single boot knowing all these security layers are **active and verified,** all in about one second. + +### Conclusion: Security by Default + +No single security control is a silver bullet. The robust security posture of NetAlertX is achieved through **defense in depth**, layering these methodologies. + +An adversary must not only gain initial access but must also find a way to write a payload to a non-executable, in-memory location, without access to any standard system tools, `sudo`, or a package manager. And they must do this while operating as an unprivileged user in a resource-limited environment where the application code is immutable and *actively checks its own integrity on every boot*. \ No newline at end of file From a353acff2dfff3cef33a57580236ceb661d50513 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 29 Oct 2025 07:32:56 +1100 Subject: [PATCH 04/28] DOCS: builds Signed-off-by: jokob-sk --- docs/BUILDS.md | 82 ++++++++++++++++++ .../BUILDS/build_images_options_tradeoffs.png | Bin 0 -> 81699 bytes mkdocs.yml | 1 + 3 files changed, 83 insertions(+) create mode 100644 docs/BUILDS.md create mode 100644 docs/img/BUILDS/build_images_options_tradeoffs.png diff --git a/docs/BUILDS.md b/docs/BUILDS.md new file mode 100644 index 00000000..2a0696c4 --- /dev/null +++ b/docs/BUILDS.md @@ -0,0 +1,82 @@ +# NetAlertX Builds: Choose Your Path + +NetAlertX provides different installation methods for different needs. This guide helps you choose the right path for security, experimentation, or development. + +## 1. Hardened Appliance (Default Production) + +> [!NOTE] +> Use this image if: You want to use NetAlertX securely. + +### Who is this for? + +All users who want a stable, secure, "set-it-and-forget-it" appliance. + +### Methodology + +- Multi-stage Alpine build +- Aggressively "amputated" +- Locked down for max security + +### Source + +`Dockerfile (hardened target)` + +## 2. "Tinkerer's" Image (Insecure VM-Style) + +> [!NOTE] +> Use this image if: You want to experiment with NetAlertX. + +### Who is this for? + +Power users, developers, and "tinkerers" wanting a familiar "VM-like" experience. + +### Methodology + +- Traditional Debian build +- Includes full un-hardened OS +- Contains `apt`, `sudo`, `git` + +### Source + +`Dockerfile.debian` + +## 3. Contributor's Devcontainer (Project Developers) + +> [!NOTE] +> Use this image if: You want to develop NetAlertX itself. + +### Who is this for? + +Project contributors who are actively writing and debugging code for NetAlertX. + +### Methodology + + +- Builds `FROM runner` stage +- Loaded by VS Code +- Full debug tools: `xdebug`, `pytest` + + +### Source + +`Dockerfile (devcontainer target)` + +# Visualizing the Trade-Offs + +This chart compares the three builds across key attributes. A higher score means "more of" that attribute. Notice the clear trade-offs between security and development features. + +![tradeoffs](./img/BUILDS/build_images_options_tradeoffs.png) + + +# Build Process & Origins + +The final images originate from two different files and build paths. The main `Dockerfile` uses stages to create *both* the hardened and development container images. + +## Official Build Path + +Dockerfile -> builder (Stage 1) -> runner (Stage 2) -> hardened (Final Stage) (Production Image) + devcontainer (Final Stage) (Developer Image) + + +## Legacy Build Path + +Dockerfile.debian -> "Tinkerer's" Image (Insecure VM-Style Image) diff --git a/docs/img/BUILDS/build_images_options_tradeoffs.png b/docs/img/BUILDS/build_images_options_tradeoffs.png new file mode 100644 index 0000000000000000000000000000000000000000..43403c07130041265948be589d6045a6ab92709f GIT binary patch literal 81699 zcmeFYRajh6(hq4j{lH~**gK`A_Z&^ao3zP1oHtn) zZHa>pqrxsMvW}8)axf_5u;9lXbzqv*?_qi%ISHDk|{c5#YZ?*b?i>(+rqt)~lKEx5zdd>2#>%*9XUWKpV zgakLrw`aZ&nl-{kb+0`p%pd)3UhCWP(xI#)NksRA0R!d-@!UlM3l$CQv)7e|L2xLo z+7Oo`Uk4V2=!2~PW}FVZ%?~-jIQ~A+2E&6+NlMx3vNL=qoxCkxwRUwlSNWoH_UZa) zA$?=bbfZn0wf*+5n)TgIse4vrt7V*c@7 zj9`-Mn!CMH)>Py6;s%1Q!()Ql^G{-3OMi!glrfBTARmp%Nwb59Hl;-Zi=6Y_t6LD$ zMy&RC-eMZ2O_@??HI6M)0R=Kj*iJC!B4w7j4((Ohe{!mGnZ!nw0x3%M7rl z=sa;hmDO&H7mY~CKBPrM7RW{ zcWW(ceE8_jE7MsLkwrB6w?_xUkWD08znXD@kxiX02XXyUIz~q9|IH6SX#oZ41+QVn z4>7|*kz`RzL^LK_QnrP7%#PrR`#Zf;E6=w;)%yEg{6p z57~hIy>L3T_q)FXXNIQ06E7kB**8&G8x4$3o9IOtaW6jKJ}oimkG67A^Qvf9w|e+P z5Q5YqjB41wZw&cF^!m#OiMmJ?R{(yMTg6gJDOgW zQQN$Q-js|@_#Rh8r>7 zF7glfH5aKc(+^dY5=TaaF`k*7t5VY6IQsq&WH<2tJ~n_b?0)M}l1@w)-9={JK4mB< zY;svmHzjT#Hu&(CaksR=bZL2a>C8Mj%O*B&P@}!22Gd%fDD6QW@CgCKL~xeT?zY>s z=j_OxfZ4-Jul7RLg(J>z{b7XUdLDcC;eo)hl6>$*-LhIp1NR!jh_6hVU^c|}1zW7^ zP;_?nW*P&b(nVdhY2B04Zio1L0NNhE$1|$WF51s%E1jj@1q0BVMXN7tf_{VEi9Lsp zW~VZabl9shfg4P3UTenkqMv}8io$30rL@z0RGaQ8^Gt?Ks`X{3#Fn@3nZh2b5|M;5n*?IwmcGM!?6!s@>zQ&*13JHx*dac?`38R=4q23HV}#vgQu+3r zDUu(x_lrF^WfRYxO4gwQeZ-)6DSAEhVu#as;$6_eUxCE|qSf^Mfz`XPp%u?fFxBS1 zfJ~f+h^P>X;qF*;Pt0diyv660i7k&^9${Ysz5i z8idPAxp(pq0Vzj4xA^=Z5G7f|)PO7_sB-B(6NA7s8*O!`ooiVti-@Yx4y)Brqd=_7 zyFMX|cj|>@G~J~_@MP3eIcScet=FvX_3j6KMWdG1CacnQxAyn0&nrm-&PP*;>sRll zNuJDfAobspBtu>@EZ)FTgAJry)74))5p395K_r`=ytM^us>NMXqX8S9T*k5&9`GG+VI$hqvUt9!Ye!A4@vcSKnM5UvBzS0)Uy7lh18wlk4k#LEi&suW0v*UM0*?rb{fuMFDX0xPg7albCHnj-3 zk{7eEFsp@tvHzqxjA&TKcN|GiC9~6Hko%vpJR}pyZcOHS4oGX!K`IZ zrRfiXONv(6tMYv993z~QKMCA7>oJlTTUXchcF|Jf{!0qgcX>=Yre2}KV=2$8@8ow_ z!nJg4JNZ4P3?ivolno;dnpsf|D-lll_gPQQ@#(cZnyRv5tv)~meBB8vy-3YvmbaPBAf>U7O}bEOq3YQA6U%+$ z=fdf9ebIcjvqZ|DRr)RQ$EHI^?CL&6%1VB5VZZl13uK3La+x3Shj3^(aJ1K9eTBT5 z%>Tuk5tLE1r62E4&zy0#4Q_(kj|E+GxZK=P*;<2dY<9~c#w0}XZrZK>he?}))Rb4 z>7PA8-+AoQ`^sN2T&O?e`9$TFWg6T4nRr(O0V_hn9GO zB_4Y}PI&)^a{}y+O&JD(^+NIs2Ic=^qH;OD0LNTv?($tHE6(kMae1Wa>6m&n*UP`WqXhRnz z=1mi`P0YLV$a<#^=7=%BqPN@wg+=mnG8o;94#lR*_L z#tcUXo}r99CWn33`3lzCHN{d@9r2?NCuip#GVsAM5HD8UdAIi1fQJ!mM>z|}w=*Ps zj_Y;A{k})z9}->R!7sF6|ElwL$sdjHz2Ghr5gvq)D*KJITaqkUekecMiar~FS;-9R zzafH|gHue?dVQPZsur?itO&1Ai` z4ehk!v$9*2wEq(Ym?Y%RblfP7(hpu$l@cubmKkuV05?F!I%J zL>Z-Zug~*EDC^bE8OR69W3Tq%p%gfz&BFNNPqZ2-T3B8KfhY&}-#PHcpFJ*f43Q*-a8?jp}`_9z^b+ z!Ri6d{EItl4;>H!1D0cahr-lbLV_&EGe6w;ef@&2GwmV-W=ldir8vvTbvwKU`ENSd z8`ExOqdDCu0ZLHYAO>VGj7~2Q0<|=%aeW49@6EvHe<+fHlC$3H0tZcQv9>_OlMHN` z6a)P@Bf!=>|0@bI)<;2A3_%Or!5@S~W6z@i>yQ)M1XgA36EH6HZ;jAbFo1;Wf%vn; z;|J#YpD+FaRX`sw$;>TGuIE3c{^tjFRDjLfJwh!1^WxusV*orWJ%g3F_-{Gwq70C5 zF5PZS{QIVPUQ)>T&0i`!(XNCpY7uLqbZO}HwP=8@mU`Lp6M=`@ga8A`bHA+ba!}Bm z?B{A7V3tL6D5{zlPpAS9K0ppzSvg?R@^45t0Od8psp^9O5eWhbbOQ;dNOLUkpn@gV zMTFNAA_FyTGhl)`ApuJw==K7z@LIw6R}yjhE7en?QQEyh+q9jmhq3C| zgD$($7^FeS#nxvD2mGW<37Ew(j!YNuxd23DE?O`FlQx^rNE){k7RbV8`|sbsYXr=? zuFnTErT&Tv3Z-rv7^sHmU)a;25aS4}glKs)e3lB>2rOI{==955P8Nd}cRM#ZIk{m4 zB(#C2j*}Xl*1tikpUubA@5esQ(>iYr#3R_}wbFsP|1XOm*g(K#4NVc*KUN;7UKB|0 zjr^4R<95epUvwmzfVF~5z;(|Fua)qPA{Q)RWHf*!m^o`t&;vqygc3$kD^nw=&;pZz z(FL5X9IgT+r;p=l(#h{DM~uF1Qd9rtqH}o(>e?l95l43cFOB^S$pU_| zP6a5_7laSBot!5uM=CIX&B8M7YV_pc6%sy+KA&UP6XvXpOh|Yrq}8XewbgzQjWyJF zCYO^wX5TV6X@;?Uy(t=Li$7knYsv2e^(7v#KQbu)qNi$$gpijM`W$RHG-X#AU zju~Lj|Ci?*z!D9G6|wFwZPY&8^NVC$W-P_B#2#1jqJ$ODBK0&6SJ6dq#4LDbh$ zZ!AOZQIL%9nQ1m=EU|K9i$mNLRg1gG*5mVAm*6Td5Br8w`9A{VH};3K+qXnLEW|K& z$+cu@d-PBxiunQpp0S8xhU#Y&xzpMz(Ztl$Xv_1NPbgz2BX~(}GY_)?^LAg|bNC>z zO##UxYRDsee@0UJjCB{EX6nY&%|!gWPIHnX(UYWLPt*x8P5?+9Kn*SKZ}V7C3CDNl zN`{k$S)Tl0F=+lKjw<<+7I9_u!Pe1FL+Lh50rA@pX=yyq#haU7Q4ou3(gXM65}M!b zYbG3SM~TPr?Dt=wc4Wy2*pVG|L_gpd<18p!ySq1m;Zw8Sr9W)uDu#^r*9p?#v9J2V z{eRk4MV!XH@ez`!6kS|?zI-BTzrU0Ez95wFEGUB~%F>WVn?S!QjB4Ys*z5ZbzK(%{ z+3Gdd;vOTg+7hgQ;uBX(OXk)u!d{o!rv8?82}OMhBjyJEye|(`15T4Wx$u4hJ!ggm zbhZP`k9*O$MPYlN{d9tvLPMe@bn5Cpp+>A?syiVPiT{H;cqrU;k>^JP4pc0;URv3X zqo$^2rG6uU)ubcreSIlkyXfVA*LD&@fFX_Sv)v;MWO&9#8~4dN)NYY@m!%Blvln|# zlw-oS--UKolj{P#0TBRbMhk^Wm*oi?;6Yc;_~rR2MZDrfw>Y(mB(M2G7tBYopcjTi z=}%Nipv-ib)mX3{q_rQPjegu2UC4(lICNNm^0_bdiMbvL^Iu%3g}U0(gmHc;u+rLC zF?;)y700co>*ABra_jBw3?dZe)%TnsTPsO8FFJ<^te2zl3a^5yTKgz_qlm4lug$Zw+uo1&#tFtlG8FZ5!)y^*Ol7>p@jUXP6_1r;fqJP@z*;iK)L z@tOw?YlZ}EAB`(?qu^wvWo@l~c6~2~|AmsH>1GlGTXZe?&4o=mf7;tO3Cn3lQcjbed)$vIGc#a!`X&LQa^gzDjB8>Mr_gSaA#P znps*bx@8LZJT){HKOLYLTFyvtP)Ytyf{5vrjA+KTpnW9HQt7p~1^pWj20Gp=%4Pu5 zf$=a%h2{yQSd>1|g-O5rl|w1nB)}}mxWKQ@XXN1`aoL~JD}`Ue06vxpgFLofB?x6b z6P8Z->S~TDy_=HFQXfIGMTU-w^#ecw#Qo0P1*k14z?{2T%wM>rh`kOgnzkd+U8=g{ zjcCP4O<#6WGY8Mp8zCE}j}Uv5#|EdhCE!_Og`N`*3WO?>KC~Y^8wUeJkOz`e1_bd& zAfhP&7y}?di^u6~cv#qE@yXrSzOM*(&7wco&}vgNUErR{I?BAeZO*)WW5z(N+=e>| zl+uB-N5{(kGZ)L&7hCK%x4&6!q2T%s3j|$d7kO?ZFvV>cjFemWLVoiSd5Pc}Z<|upcAB9@7Y*fQu?6-fCoIYbbam&y?>e|!b4%DhX`0UW~{PX;AIzX0$ zByYITfYH}*0+6OGeUsqmmveOD;5N!1?x;<7=A~o4{TTnuoe)|i zh7S&)0VJ?a21oARA2@G-)FzCnx`OmH#dvvsUdSG@>#`pB>v-()z*>PGq97k!6P)3L z8}F&d;((;e#HZ$vC1?Jn0mX6o--t|5QgXhR=KzS23n##5W#((8cfN0LZQN6oc~7Ar za(^X!R|^X0^k)EyVfvKMsa%$x2K{?0cSH}#s2)?l?Z8aHL4XLZ2t@GgKvNH>I#OWD zQfK34XAyGsK*Cc#E1b`DFkVZI+v1c%m|_yg_ZD6rb#7!|kt>5Yy$oCXnRFcT{G(LN_<%`vVh6mQ zGM-@9Q*RKRp3Ov4T`_{QK)=k$ta<$`idvGea;2C(ZAggI8Y1Ux5@Oi#>yHV4_Tp-`4FwzsCaTF7@wizR*1y+Sh*|pXxuE0Eu$_qut{&P;f-X!UQO{m>?*p zz5=@}mo=^QL57?uxcJxJGHZjAg2-cebRdC@=wizu-Tm`qu&gbbO5+j(Arma!+dfQo z2yywslIl9YZjA@VcaEKHF~{E`K=LC7XALA!r8G*mfPn`FV9j^<`xA&`S*mMcR+iP) z*0u(`fy)e9M-L|>aXXTlUKq+E&_4Z z=A&f^PixZ}f6nz;NVh17`#|Bc(<$%N_|G1K5&>XdC+4js1f(Pg8*#7-^YR0;uspQ* z4FBdu<|q0_`EWjro7Gx&7?u?JPW-Gnn%o5aL-#KYsyi}Ma} zBLolsY8tg+z^Jn-J*I%|j)P!}yCHA34r4>L@V5^AN)Ylh5Kgag3$RoCACE%^!>Sgw zScn>FEkUO`sc+-+8cQZkj{UB+?npV@maUl`Wr+Sc05b)^@TqGZjQ|_r5{JQ$-^1)q zka4>>9)j1h>}F#?$^1yT%IEX$hA`VzWiceh9I7UO)eO>3e zep3rxUp5Qnn(hbh-u%Io>9R2$cGBACKk=)1NAxPsv9QlNQ>O6CN*VXFB_#MiWKy+8`#*DD4GM?1#3(2UwO4uE21F& zL}1-%XlU-k-P@#+i|7dM-}mYgBh{in(R3Y}oMvGHCT*D|=I8f}9C0ui`c5PeX$s$J z!Pufp@Y6$VmctIoK1aTVyy{W5;AO}2xdL2Whqss4ot;`Csr$BqSSc46K4r782i{J{ zRjYX6!NSv^2gKp>$l`$UKRHk!0AIXV)78p=cv@*-?wW6cA|AtqOCFNvkPt0pExNq7 zi`K55&gTa|Tzteqz3GV0OsF#!e_wP99y&K0O6Xg2KI{^Cy3b7XFZ1kDSU7()hOeGE zI{xkQn*rMy=?5RUjF~uxqRalD!K=K%;RPlF@*|YaC@TXiJA;U$C>1>qmaRj4%kg0s zMQ8lhrv^W6$k=&Qv6*lJhJVM|+RAtAR-UoDakdYJhKz}f`wlAYN1_&gLg+tl4BQ*P zVXVal+~-;n$i5!l+5kKsgbs_td)sHmv&6=6@8ibYrY< zG!t{tpJ0544G63!a<_pgbhlF|s^mgb#|jLgi(bL&?- zW!teqH*Jr9Lx?(_o5m2TNtkTHE|)IEKYR#s(cw@{PKbQx_7$PJQ(lhRE*Q2;AtLFw zY#1`|^GWgpwKrp;27;cKnO!37mO#x4t_w(Q9UNncbYq}XOS$bQK5?ZzU`x5ZkX~Xe zkf?wvdrcz8MwySe5Bo(!bI+>HN?G<2Wj&ngAR%H~Z^V<;$~fq&&mY-oKn|M$f2MNM zdW4|SIIG-W}yA)n9>TKPD-W5#xS1u(AUR+U8^8^n6rg0)5 zl**btia`>kexJOX@N;vPbAw1A@V^&Ac#y@^c@czEon33lf9VZ`Sy;3MI9tO@tNMu0 z)6?q|O_5~WcdRwHITu$Qej}xqEi9zXhP(s6#vu2?9gu8ZiCoKj-`GyF)9h@b~o3QM`w*^Dc)2h;?Q6q6eWe!vR@G-@e+1 z*V{vcd8%)eW@wW?i}w%5M!r;TW1e9evIcRxNxwK?p5(C1(2xl&&VPk51&O^9L1Bpr zA&5Ef>q_u`xIT^!v1EzS^$2%1)mdqF8w&OJ7xN{)eXhUoXBNbLf^>=?v7F%9c$1H4 zU{rEqruFD)s8Ll8%dv+YhSa_!e@o{fWnAii#T%(0s#i0Y<~K`wXDH_cPJR0_jt$r; zmn=Z2fb#)#3AiJyfw8d;m}JDlA$T&jH?Leim2`xWoFWGnU(I|iRp>p@#5(s__(8M< zVF=>gUQuRcD5*gA-k-eixQTexptR!62*y-bpP7785^jk-#kFEPsVwuf9qgh~+!jP5 zZg@jFoi6tDWhQcI$ojy6w#L3{TVOK`nhvV~LEMnr%0v$kKNpTl;q3+{SIL8rjb&%Y zLs!uqR|y(D;VHxO0)hj^(+Xq}iORZM^V8vLg5!wsNENp)SOXKs68;k0VfiPi%V$px z9`r#eC!FjQG@A}6)qMi*Ma~^p*{1>zblvTV`JPdQWl7l-oWaGN8i8?B#-a5xS!J2L zd(fa42*haMh`UhD*wz);Px>Sm(rjr4(Q*qM>G515fd9UEm+1kJsJ4p{&1tr#LR- zQR1eLho-f8|2Z7N9r$wA!MRN(5Dd7L7(h_bk=>8*P^x6fd3inFDxR{|$_lT28j&2B z^F^I}T@k3I>E-2hp@G0-ONs%9alPr@UD8!w%(AkGv!SurNXf(Cr<)Qh6G0s9fR~c? ztf%`)uOc;4DH(|nNRGGT)W{_YHoDz&1(l71Pn%$C|zW#6I+IdjdQ8w z$oZh#jdhY>j1Qne33$``I?vH&Vp+8^N%rln!)33MEK}_gw`Cbe}S`?rDGH1naZR1&s>iB_ZCH?sm}&J*2>Yx zGY7DmlT;GQHrMjP$28B0iX=nJ-PF3haqbkbn!t9=%-&64zFVh-bB)TDam#HH_G%Mh ztRE@EY61>oK#OHuS~c`+ifhQ zZsT`E_JH=iWMRmqJ2QbvDX+cnAjDzlpd5GLvRb;Fm_+Cmpj&rnsa%lA(UkzDHWX&- za5Ol)J`0gu&P7L$r*|zm_Q{n=s+=sKVh^XQ)*a$)cfcdK>%#XekJWM^cFB{BrM8*N zt?a&%^(Otf;lo6qSE-MW+eT#gjpnR+!yu*-$#4uZb|X?}Lj+b}WkpJ^`RNw>urT>g z`Yp>>IL09WcE}*&tcbz2zgy?)buhBb-Bx>S=cxpt)VZulAg;adN>n;2C?OVN; z@WAK-y15xnsSg_V07b8q5t?B^wwv$A(<8jRmcQZW@>8Y=wCxU5l;|WZeW4c|(?>~R zD*2S4PX;qTR4-`|{)_490o>wj3(AtI0BSM$QQu}G2_Dn_DVSRyYoec7s!aHoMzRLYYKlFt zs`&V54JNg@U!G>Z z{x&eiLMUFaIrgbn;Ez@$ktIOvLWh#{ai~OvC;&|23x~=4TQs!WJ)~%JczrMvO%e}U z4f;y7NvXA(azna#+(fxd3!1Uwb2a35QZF(`ljh zSfVpuA4Xo%0wcvz9<5+=cyDgCdc-U#zUQZa!2Os}fzYL3*T`9Z(b*v-`(|h)*~t8K z&}fAxflx4wK<46?YxX-_ijV8c>hhCP+PN_zd=dXnP+;Wq{wwBG;pV74k|S2;4ebIQ zf-Xb~#^>kfc~;YeJ_puDq~MgX$E8#kDs>Am(gDN|MQedt7rS*jHe?)YMN$|iMJB@J ze%I`EAwjhK{Lcl(p_Q)6B$b+mIh~zl4i2|rhU^$lXuU^l9D0<)B^eq<3A=_3)M7)) z;pW5<5Z4sX9!6a&b;B0B<`z=(Vmz=;;JlvB3zme8r)R+>t--1;ghu z_X`aNO&{J!#2}R%&My1r?3E`s7jHj##xi~VY>!&Uu8G&dSX!DbX1=PDW0pR_1B7)S z1t6?D+?gE8iCqAcf(3@kSFo2%3{~Z$9e>2i0#FAv??_E(dz-AYlPuxt!7Sxz%wrf5AOpAO@O4C^%x+KrdlN0IBnX?k6fEN=zBijXvQsuZ zJZ$R6NhKH_wQLo#tA#b#tWg@IRc&LJJsp~P`By_U^CypRn*XCe1tsV?wB&vMDa5T|9-8wmLaX!Mra|}=u-Q%@d|HsqCM3gqtP z+FGs`K)GG41!#~{o!cA7hbYITr}?WFk2cH<@M1w0th(NXE& zrzhwf_2*ltiFGPMqY=)8E#DD(-ZJ#wMnWlBK_h*aeg3H(pdjpDo2CwauDQc|>Ui3O_ogCfTb{wb zz~R9<6pVd`2=rv(U}HWx5>FDSpsk{I1uB{d0(`= zS($R}jQ0JhAs0sL`U2^Y8QV7kjt*>ShP~JR1s-pwcqfgA%YS;`lRZSQPNUi+9i?KT zw5eI?ISH&_7MNeyeO_$zNm}eP2e5b?8BjanTVyVHl;Zbd3Tltm_XV{GCVmK8e@1KP zW=GDX$;$0GH%8*@`BfeK4ucvoEV3G_j9AcuQtsI(ewyB>k_f&zq~4!SMCe!Q(3;h( zO^&~gO$-l3{u_LyNzWj)ZgW^Z`}Xqz-)uc*uYFc6(!MpomVcc4-Ct&M^0K?s~lh=wsUjY~(fCy(^E8KuD zbCi#YK8tdvTIHcfMn_`hnpeIvkT{OS3ngHrjZG^@SsF)%nl@0}bx6lHftM2_0-ax6 zyr6oPx#otxSXxCHF$<`enI!m9M82+)>&v&UD|HtPn_KlS?lUp)GPRzp_H1i;E!aSI z1Z>yEDDKA)l0;Nb^?exg#SOktB~MR4eZPx}eC%W9A{H|$lJcAaZcbG2QWPO<-D^Q; z|D^Hd@3 zcIXHRuF$E=Q;(;gS=~Fg>V;t^w1IEzwe6ht(A=6HP_npyqa_bc4#>E-*0tG$@UiA& zsoJC6Tl$CtL))8E0y&ETLUxmo0_NkpP3^O!ax&d_R$IQiS3<5DA0MsyMPBNTvV-lL z>?Cwfheg~DvwV4)*8*URSek6LaaD<#g0#*K2UqD6n?pl~7Yz(7noq^B1-ZS=A~CTq zhYqL@=?-i%KHTQimbg=Xce!}$pCLPs1fc8zG(}}mwnay|jR8nOL$tV(@>v5uvXmP^ zS2YcI1gN)$&DL+8Zr&a-3%;Yo)aPtGZHKL%n2~?sYJNDbuWTO96kVeUsxu64pkCf7 zqR9|t5|{+{ncUgUp7;ouAK4Q7w~1{%lyOW*BR>u@=%h)}#65+OAWh)D8y5kv){boR ziy?1&*{gR=a^)%LK*2ql6EN9-w-Xb5Vd=cety|1P!*uKDE%crbsu}_jlOWfKw;{lY{&Nqh5)# z_paeEkZl1q(O~|^v^}YZ0>~@*z;%pR55k5z9suYI{X-x$AjIxhWheoKr}1h+1Kg{{&v`_yO@q5HX^nX^vBfaHi_Q*hSq z;jmO1EIo!T%0qJIRU-i)+&I1M34C40~ibz2)X*{+i|b~=;0tMTdZWY94S*PrYcd4FcsR+*$1Sv^l3 zoZg=tVqqln8l)!q94lzIaZ43t)WrgREjnvnICA&{SLXSn>IH<#vw!}5+r~Bq;s9uUp8?8O4V2ClK|MsT0k_Ww-Fwb5g*$DwIExUw0|^ zwl{cBR*9(Mv#vM($%h6*=gn7I%`8m&8b3jb{~HR%(lAVwbT|E^Squ)~of zY6T&u;ZLhQ+V_DT|8|Np__5TRY1~qE&Ix?Q;kuw1WL3VRJEI-iubnjV-E%Hpu@ z)HE~?KXaHZ`vKW)xS5N+jWp}VejrpFfW14<{y3df=H7%v>kq)%fx zRU|P|z|Ak9J`Y*Xv*{8K*rxXrfKw3!fAf`Y)S8@{Iz@|BJsXwBYJS=<50%-v5Qmsw zJ+7U^ZLFmT3GlhRgjTa_ROp`3be9U%Ue7fa|3G=~Br||kZ_DTP=h93^8lqX{2y1E< z_e1rG61}fg_hPa40nO+p)@%HTo1kw*vk6x7<6TqjEJxrlaUv1dY<{j>06XB%Z;4b~ zX&aHNgnE@_;j9mz(SDzNPJeT>aPhQGNHR9Nv%Ni1XhPtl2a){8_Prfbzo6u&}T) z!#1x4d_Ch&4<{|B-8eWnmg>AyS<$We6O~690=FImZBHEdZ(SvV8U3fECA1Q$n()ta z1xYK386JC@bnG>QDw$&_@RJ1s6|7j|s~3vxa? z`Ka@au~ydWvgV6OAO1d?YA_Ow`BzxQdIu$mgJQYgkFbnAqt=&A&?G4gL|M{@O>L6$#fcvwedv zpMu7(Hq|T?iK3NBEUk!iSN*MSeh;yHLFG;6(iY8V5sWKOYYXW>AB}FY&C|le$mY?b z{km`JKYb3)v6Bm+1{9}5sB|fP0oub8ee<3`&)7bS%;zs~V1?B}5J)#Hub}W?cmY2N z2frb+Z3sCe1F}TDUE=#cg?>_hUqV8WF>pULFAxN2SzW|u88{q}VAp>(a?9M$f|ow6 z>qH+p6=;+MUp{^VQT>3C>hL)Fw%?X$an9{f+khzV+~KIIt;0MxSTD(Ej}1gqdI!w@ zCxF*l3Ij?kw+NS|gsnsg!20YSl`R27Av#Su^E7Ixm-79i8o6uBWHC!4eHxH9ium3) z;u{my>nriysm-6#Mr@hgeS}Qb7rF0GA2v7AQ7I-7u!w?xt6SEy)u-LeDuC%2Li_1n(=4N) z)Mmj9sXcx}jks$eKWKsju`=^s%OAeCI+X*C;#4UxgKn%Kb_KF%H7=Iu+c8zO5Lv0@ z)0?_=jYXq5S~n-zwiDJD#!|L&dIs3)kx`x^h5lzMv1u)xZ3DC$f;4L;O>=yDieFqG zw|f0`jo7nC7ZxK~X++K^u+=t{LY|>=zm^KjmJ)gnN5jlka4JPa{}>Bd*SA(X zI&^iU*KrTHYiECN=F%-Xd~x$E*l+5_?Lhi|cQR7lnWtudN|G|91}w-5y*i8i!lP?$S#aH~sLCL6w@6P2`upYK|#cT4gfRU0mq_$+v_4 zL$5Fau2W&qaVs`a%5;agJ3`E7VXm4@-up@Rr%ro}B9Yryda{YP@34#kGlmy}nHTnk znlZSJ*iffT>1=8uXkfPoVhVSKnb&NuV1uwZ5ai$yu)<~JA#ywWivgC%WfgA18BHkC zm6ZZ#WZSLjj@(?DM5o*;SG~Oz4Gt$?$EiNt1higb_>;7HIY%e6ZYr|*$W}|6AD#0C59y00o zlC|gwDVV_SbB^9_S!lVivPM)Rnm)=*&moXK!V!|h%0X*$<3 z-v8?I_XHO&@xSW8|7r^kfF7h!thvBTxusOgAW&x4syqvT-%^}6g;BqBHZXRPF}RDu ztv#<=ul;He!>C!t{gbc)V>XBiB;ZzS?q#AP58}2E=uk9McqA zYybscp#6f0iHSeow2m7Jrv79#XjE;&eAKnAWIJCBu9~h{^5!et9;rNXdbOu|Pqvrg~7qym6pB z;ZO1*Z3LKAj1BcA)=^*ta zADs2iY_Y;G7Bra=1=SKpwLpV)Wqm(V`4k;2mg3ZLP-GLmUlj3q{ApP6?$dRGzM?QT zrm8_RDq)+d4*Y8x&7aOuZOsJ))ql9mU_xqaacOmNTBDf9EDD(qiV4DBkee#wc1TPa zbVXZiiu|;G1I5$iZDId4qt7LlRoWVF^_aQ3UFIS=8eUmnhlQZ|c-DRX{wnVVo;fv) zhdj_2dH|*^gL*s-0%%xvTAF+>4v>7J)Goj2j0KvMxvVowQ6LO&VR;!`<-zY-fif^! zQX2K=o}CR9-2=wraTv_f_ce74;GYIJMILQ`9|zbrCl>EL2!KFVNeCaqb!oCz>zxPs zw0?b#I^`lwtM?4am3icwd-YR+uQJ#yvf%l$jZIbCK+tJgIE|YC;OB(U@}4Pt(2iwF zCqTwnaY3J$rGy)5czTt8N~(J>8OXsIvUUY~*lVFqogg3mG)={1{79?5R2Xj@a8iTK%UNU~W&P*nYM% z5WX_hX1aeAy(i{=Owi+mI8QSixRaAN6f6o{adAJIcWTf*o^&Vn0TV2?zaOcuX1)Pn z}X*g$X7BEN~PkLx+WtYk`= z=2}(MP3}(=xdlBdiJRVi!A+i%|H!m;%cG6V8?E@ZMpf|A*CLX!dXM~r2JTmDG(*c% z)tm$*I1gqZugoON(v|5VdUG;w^A#(=N7*b&G$G1h;O?_^R7Xw@`IJfZxU*@miYYJJ zB=?`-&z5Q2Er`7X2;)|+8}ZIo|ArY6`GQbj?q-YBPW{lw{5x zszsE4Aq*=hHT%=poB>??SdliNCQw>IXfF@`Iaa<-WN8Zvh^gKuCgDw z3AosYfLlp$g?V#i{ROM{a%_k}krhAb=!}`|rQXj&cUOTmHkK=^nAbnKu3`P{%k|0O zDj1BcW+>qunAV0+G%}0w`F{S?XT>IG@kR#10GC+E-^p#vHg6sMC=j9D`$z)^T)-V{ z7xjH%2dl5HhP zT_Oh6nBeMLPb+O8PlNAkjPP2Yf5iAAphFWWf**QErS?y}malHQRT@Shf2(p95bV|WQU)amXO0(YXrDfwKP6{g@VmmHX*+%uq43=LDcw~H!*L}~y3~(l zEu>=5uyX=0?Qln$@#6Ic>n%P4-iaQo&1a5A|{C*n5m-}cZ*i#GTQ^R=eB-VZK4 zWnvEkpj%3@{H$79#HRiGH^g*4H>`-*ppV}}t0VPykt-;TyBwDuF#gUkc9k9}OS8C< zE(*%{Pe#1<0k`z$3+F44J<{hjZLi+i_7Tc-4b2DS3EELPLe{(5$({GV;kp1E z?bUz!a?klY>}qS>xNei>ec`KZLSv#>()?!x0f&9gc%{ z_9;gAk+wtZAxJT{M$Q%W{A(}ubSl8nipBprTA4^|yX*g+Mwy+MFiv27O~}Zc46l%5 z?(n^Las9@nPm9EPxsKc?gJ zysx&t`ttCPSeY!Wo)+3aiu}|?(b@uWnwNX$a~A+FcwD?s`4q}iFYi65Qt^e#We+Sq zvbr`zm~**atLiG{25A_&>;c2kuPTuuC+yt&VNmwrv|7 z+fF*RopfwYNC+B%*=9{zDIsc&QUR77^YpWV1{+tb%wER~e5P%Ebgc;N-)>5t| zL~N-LqK*|C)QUcPiv;jUna0;Bi(*3OKWSxT0!Z4eC8Vd$1o7$)*Nb7*7_~N~3`hf3 zgR5S@MHhpnX@Wr{iB8M|BvvKQL)5Zp%Pxx>JrN`j;HbBbVs$I zQ8t}zd_WDU|GQtzh*wMBCWRXYuU^xPIZO6e2a)ENP4#ZccT1i0K*w(=Uq)F^OS-Mx zGMOxH1cvvF7tIo(@>9c`tWvtw3Yy-yWp%6HZ@qeTu_ZlM;(Zqm`>O`M(7hEpk?|AK zB*C%(uJ^DpkMG_yME)U|koVW`Q)+A=M0D{r0&3aDvb=y%xZ4({6LI1s_uZ^ZnTS$4 zBeh%Q@&B|VMq2W5O{3=P>}H;y1fV31t`7^A1#^n?Azc5;3SSSiYo(vEk-ygoT|yZw zAuyZ4u8BV2Uow9vyR~X+GeqsX*Ul^H>-9+U*#OH%Gdr3N4+3~>thm|b)LYy^&qhFza2!R2dq=U z8Td+yTncYZYfVi!N6h@R^xtmZNhsXGcU51s(wfX~;t}?Sm7|{?c-&3~+L)Uh75jva z&n7VzFVSHYR zEzoHh1(CyUVjld6n&~e)0j9LGy^w6OmQ_Fv)(Y@B*bt3>G;jpH6e04T)>*93e-R%j zry*PaFVW%-u#FWJyjI>R%562eUom(UwSNVleQk%uM|l4u!xj{J;~cbh1!8kH{zzSNI=f zeLOroL;%7WH$p4a)up^nVm|DZfsfxl30`8IenUwfRjSNfw*?b#otK zAnJ9?_&i-xWUSX#T3H1h>Sz@c#riBMeiRt%f4I9e95Tj+24Jjh%!kK$(}h#gD=mGc z!j163*@N^|BAQW6hWE)bB;Y}PHMhQK-)%f-Z0(iyxpxgYz(iSl*K-bXk2%|eW&U`o zgnx#rC7E%}uzCP|7hDBIrnV*SW0}~*_({r5j+X$+Tfv|PeT2SBh_8{(+}imvBhd`SUii}{taFk8zSJe1mr7#qVHoaHM@p* zMq+q)NULY7n~!ysq%-!w$>F8gD7X-A*u?c1@%V6i0~9rInS^@ilUMtMt|~;KOpQ~_n*mWzdV2O$l3ayQ%^=`$UY*x1l?|n zGga71uBS%5+)9ookQ-v~`nPro&^#8ugEh}dw2?dIFY#hhI_E=S(On9Iv;_Z?r3mj?xbyNwpRB*EM7 zWY7Gp z-#HadwO&SBqfV;Nnr;1AEX#WJNDde9fl&D&x__gD>brX$J)ziUgiiG({4xbzeFy72 z6$xd42;DO9B})uqf&ZOG)hA*j@^|VQ?52#MbLZoPz#q>2vC{cuFbw zYNhZllfA^ia2l}(K=_?L5(2OxO)sm%r#mL8R<~F-Vfpr>BNDRG+L^^<4^K;^%V9(S zx}>ot%UHf_)`sTDlEr>#NQHCP2w!oe(r3E@fH(4~-j>iH4Y6-~Fb3a-PcDx&81c&0 zoN6kA2o-~-@|K3y`PU` z+yQ~>_7gaY2?*E`%>F{iC71_8On{2?W)=ArE+&CBlRnka@Qlv4NhsCQ|50T-^C+LO zMMir#yhCjxN8=RdGZtkKk~Sb5c?-07*%kGUMib?brbdt>c|2(~8x)5R=8%*!yV!sZ zUaL(Cx&sIvqmT5a?1mzi*|}nju(hO+L0#!_l=5;C+}!rHR6p*ItGe$uwWFKycHL~2 z4%|dR+g~Xmm#xUv19)sA!9!yVukQu^lNARtGD zl<|T54W-g^Z)(v;gT$4ZR*aT$5U0Z#TQe;xN=yOB=^x+1FjobD2d$3ZNmSukL}N4% zdXy7zYwfQ*T^Ri@+JBr{?jlMWq(?C#iehbR3x%N@%KAB%S*K;Ip29ySN)ED{Dz*r= zViwZaYHFLL3^zY&ps9WbY`rcOO23SCzUS)V%p|CY-N54Sdy&L+=ycd^_4o|KN++`4Z!dzKrvwUv}|K=dEB zuR=589~w-4{R{q*ekmdA|NXfN-i%7Y@PobEv!Z*}J^))A{T22?`d#t{^UKVPAk=rgxzdKf8I=Kb{0YN2vO8qs=nGc_|6nFy+~qDuLt5pw(` zzWhMFwF#N+<%Q5As~B+|oUUMImJ>2A`G(13rZiC37jYoZuCW1OU*pZM$=5!`!Wl14 zZ9&0S=D+Z;n(287pwXb`8Bx3FZ|IIU=Qf|$gOC%6N0txa)femc)SYmC+s!M-2z%eE zh+CGeO+fHy^AQzT)yljhorX!v5kcK$T**vzcvd%%G&<(q1Uy=2*N7H#4Tly@Sz6}PYs~V z-l*1TZPj!_@ag#VS1W)|$@o?-EpuClOdfyu;4#~r_-P_(IR}CtDJ@HUm%Od`w->xBAShpQ- zDsJOn0T@xVD}7$`+9hO%p5WJOnfI$#@Z3{k53U{JO@oDYX$(tR%1y^LduAp%B|K6r z>DuPf8)ma9-S~&^c;>XQ25#iQYKzjt_!myyix?_IJa}l^^8S0fyk;ua__m<7ZLW}& zHy$OOm2@D1Wo~kLI4Ed+f9<*Y%}xu|ag2pqW5mMnprNCOW=xsmEKM~DYz{7>35fD{ ztdry^R;0{;2&LD8uuri98>bx;EF?N={VwVcn_Y`<#K+gnLWgda$vaD$$6R*5el_zv z%7o6qwlBJ$g4X9YocV0`A6ji26^lnP@^}%=pej3L)gz0~xXQJU^v8y4udRz1@NaxR zWN}~k*X^*bSI@mgw$mq!r*Megr~;~cjM6XWC)TWnBdRbArR4F?dY`gqTCiQ(N)d$J zFp=&mzx~7@yC+-^PpeLa|i{PxqV&{+1$q;teyZE z$TW^-*;qIqv+J5xR2gAkyVH75U?8clC#gEPR!pU7>;^Vv5gI)~q3G?FQ2P$8+7VwU zR0O6vDFFLj4VC=sKRAGE{Bp&C^v`)uMi^DWyK>*gZCc&ZVo_ioWxOLi)OKoJ=T_F4 zrrMLlBPD!!`B6S68>b?x9kz5V?Fj!%58*12&kQD2XY)BNFrfN9{K%EH`HoyWqT>p>kbyHEr^u_W**%fGL{$Zo5Iv-NZ`PZda ziye>8?7g8XC?~UvS724J?I?a7X=VU5>(=lX4-gI+TM0f9O9>mcki+Xq`qk5_>+{nE z>&8(^&gm<$vF(i3BK<^mPE*SA>NU7zLjYduMO#mn@*mpQ)&T^#T%!ExMIm8TuZi=g z0^G zr=8;d>Fob_mdF1(tC%|r2$?Q?8@ulFn3a5zHvA?9I2-8&|50 z?ObDi_BE0}ZfVdy2a!WQ$jGkxk*xm=fsg794TyYatHn0{+q+l>L}J3N&vUzM{O54# zxH+NR{OTRn`ILefe|vj|p0yVy^*{uU>1TOpXzz=qiEdaw+XJGp3SPd3jF+R12VDJh zp%-kJWH3YM$6)}*@wGO%?{Rtclo2-S46Yg&E8hg5Nkj3vUE~HZ4bxtT)0fr+nVwGn1q+=Q`FttF7>d+5UimdclF@}y#hqGbjbXglS*-K zZ3FO6{@P4LTJUx8`Gb7^n(Kv=+OnmjKQ>w6z1Z`=X>1j#g1Vp05r(aMzq1)WmskR;5bqxxxyDk`_i z-t3<)R1+dNA4>n?@a<<6JaP19)zz)Jq}wr_aj-F#E?C*kk+!shGySW2%G z?nf!5Y+_^rOb2->gW(sUM~aQ^j8A7@G-029zE1l<6hFDefk5Uy;u8O7-4FkDU}=il zS&yDx%kZ{%tSR}q)-N;Bt75H`7yd!z(DmCIfXY@9A!Y)EV-&*pVndi}b9xl&>t@j4q;say=ko9ix z78V$((?zW*Pz_Q8)%gvvh5v~OGbtihCc>7UNBezavdmD8ibfV*hId5AXv<9!G}u3S z5auVmXY7_EU9hD`!GC;n?aM4S4J=oaE)h<^Y}Fv(i*)1Tw&uzK?3?`J?B>>8Cx^wO zF!vibHmUm2p|CjEe?_Z0aa-`GG|3dU6ZlI8BN_nbJB}60tGb1vW~J0^Tyv7+6HLk2 zmgZX$ljd*tHmdL-hsrN;p~!Kjy7Be32-C5sMNDM`9QlTB(pbh`as*ZV(0zavnoo#3 zYGFHwPPim30&uwyQYBNj?WY^EY3Q{#myni8S6(FLu5e9yJ`-|jDr^DyZ3wuy80K7i z%hzAl&-1Ksr7aRHi4Idxh3YqKXI_>Zrq`Utevab~8hCL}z&!ia7*%wVDf@*0rJN;f*tF3OcE#cr;$J}{Twdzn)%F{$(bAoBnSE} z`K9|Zf(}8;*XY{Up1bwRn*V@8VC%S#ho$dmFH^`V@!-4O&1n@GpamE#+phC2K)Z>Z z;0zun!E@}+Hy70waAO+wvZo5j-{KCOc=ocC6oK2xv$$wN%mW{f<{{hZr(k@W$-Uo? z!y(^t7Ae9*iZ*OrT#ju__5D*Z%{BTBTj{4sRo?rRC6lt}7d**22QiTco)E(Ppnhcu z>kH4iHZ##+rYEyW?s$8gZ8k~Wk1o^pSlqXz55|Sp1s~}XmmEIzm>URhjh*`mRzDk3 z8iuN7ED<0-F12VTA-zu@6;1t*{c|e*f9C-%)r=IpJWuq9cj{;|_p?3{a?RY;D&z%K zrh_y7a+ZH#r{tf&3Ga~3am-33;1$HjJ zAJHyVzfC#MoMQJ$fdHq?nNpytMj!|p*r*5ioEYZqo{(6-LM)`wQ1EY1c=7=SkW3iA zAg5q~-}#GReWmPNz#!d^ou&R^B2b?Y0#*ogy6Sxh!1UGm*Si#7IY{g1Up$D$Q_F~> zM6Vo=-~uzxQX7Q=X0JI1g0{JQ%i9=N1Ko&zxt~t#&CRG@4?{WVKhL;`{bz=7LyE$* z&5Ek6?>`Qdr~E(uDvr+(VBf=I^AVqOR{o2_IQZwEDt!U7^VVMBKo(?^c&pQdNEeT2 z{NBL}?I(qP#+E)Fa(gs`%ARgr&`kHd#*!Jb05Q}z&G&O^u*K`h=d|{YoI(Hf>OWH?K=Pr!X{F@y8!cf<0QgIuHn_a+)v|yUKk#g7y8MvCXwJ#3 z#&tAw=iLo;A%l>&k0bUzx=8~JX1j}{?Kn_wlLvI1-*;xC&=lb*X7kLk{e*AP57|xa zW|3$tT@jv(GP?r5#e#Z-6jNYaUKN0z7J-Q574Nf6O9X`cbO?Jux5EYQSkr>9VGHCo zZXW$HD)nB2Da+3FkUvqQvZ_Eq7& z9}HoYbE+LFyJMjDlJ>>sirf@krN0GreESZ>&b~$rQla~*vq*BiX%7u3CQ&qapDGyN zq_9`YUkv|ojVPA+r%Sw-Hqr0|$Dro(ix|@$wW533G| z*cPgbv4qH_4tcmhTH`5H%7IT5QxobtW*b7&a~qGzM=-eiWlOYX+vcy=gOqVS67n^0 zGA?SRY4g@!N)GZ{{LT1P2U#4_?uS0B+5x%n>%LflS}06hT8p6`(q8UWB-B#j9C?{l zCy`fJNZRy@6*00`mi`M)MH`ldW3tnsgr45{7DNL6XSrL%{U7h&zCN<^)j5D12`xvb z&tb8Zj^~k7>Zdm;$y*pdQu&UC+Q(=jz?Nt|*J?EejosBRT(hpF}MFit%{NU%fia@586(U6-y^kh2yS6s)aWXu3ir+6FOXPEDK6yv(O zVF`mD3)Tr}+FGHJZA)T+!u$Cukk;FpL1tX)M87xTTV|?8#GO-^FVm-gNFel!glg|L zNP6$TDrnC8=(4Y6d;A=HpOx9Qveuek@v{knY+Kb*gKI#wE-b}?##}bAUA36k@c-^$ z80J^B(|+12x)`Wo7bX{N>h>EM`1dalM%!e9qy*HP*RA2lUzqb^?riSER$-Z=Rc{B- z#si&@Q|r8WW4aU>_w&z=A(`W01|YE0n(Iqj6v^)vu>*7&cZKBUasMC9jeGs;>r{_B zF+gxUd9C3pmha6*dpoElPV;}fn>%^s%zO#{n>TFUCxE<)zkbNxy{1yzXWBT<7SG7U zIpWG<8JqIKC`Ek#BXI`zb@tMMVQ4?Sd7~$XYIWo?yIE7L8UQ(H{aPcurSadvtQF1m zbW5C4mG1^G_3NsrUNgj6o~XERYY!ELJK-csOcVOdjI%|o{2Dwl0D4L|sI8BnLVBb1 zrK{RkD?K;Mgx7d>n0;ZociN&CIr^!0|Fx^6s^c;KoGI6E)`e*u1O){x2UZ>#1S744 zJQNfF?wzKY%m1`CTlR`3rl{DbFDZx|5`u%07{Be^l16ni88!$+rDNU*eBw78a@lo~ z>uv6IlIJ}0HZ!8dYG$7Jk3PdZc~Xi5JOTH&bfZqy+b9|0Nk!&Z+RdzZC)GgJ8wXYN zW$BMJ6dne45!@IODmqcyciX?hqNjrrHWhtbTaq3Wf)-L~;?Cf`OW)!)uNT9ao9Q={ zBb2jXWMDW8hgtXDwGT%)oXO#=5!N?U}Tj;<$)-Z$OnEpx5b z0=ncGv(vJcb?xw=3ee?e_%-^46Kf+EF7Vebku^P%iZS$N&B}WBgFNIIbT)^N(1P*u zML_t+y>GxS)s%xOS@UcuPNqP#nAL#UV<*26Of8V6gT!*r8N9nJ(FFow zrraP0?(CyWiSp}3E z=c71Sy%Ud!O`nYV0m^|rXCfK`8=U=DWvE?eqmbS3oa8(CyANSER%fGL9Tj(p`a=PWp)Ta0!p(rf)%rS2-}$EERz;rNkT@8r_z2#9=7$ zHre11TOfG5zg&`>QJ`DlFh=)%|eA?==w3uNPuS}M_5+U`^eqxYr+Zm~RrzhGhYard@5`(YP zv6Q3;a1wXG-bTTD{}$+W^aCYY_Y_%bI>AfmbK5yF7CC0j?y`6sc3;L+GjNiM%1R_z zuJq`H{s?S*S&Dj>Id=(z=1Sis1D)14{;_*I`e}IC4nP9uA3Q9TkP!{;@V+zBNa)Biw!uI0O{b`K-d}98M`*Ld;w!&z|VyQ+oI7c_n;8m1uE7@b>VZL6J*g zMh|-W8KsplK3K8y1!~4#ihvbS5Jhcp4@5q>cBJCa2yhSy{A9B15YM+&)Mz&Vz-BTGO)V21xNFeDw$0A474wtIvaUhT-1-3 ztZRFRfX*}SSAHt(#@^dY4(5$Cfs0Is!c55n%DN>jeBJ{0t#3z(B`c(BMsJm55l~?l zQsO<9h2ptV#GR)RfZ9EAL?9PkBSY+gT#!qzc~o&j#7XK-fc@>WTu~eB6XvwMSq=>S zHTqjI0tE>X;o)7SQfa(?n{WZ3Q4^sZ{Sad^uDta=P<*FMY&&=#p* zfF530COYE&rnbc8L}l6Vklpp^@ez?)CW+4`LKjeR)^4h0zxT7*g;};3!n%9j zGHVVP?iSyR%%*RNaEz^{0nt4pmSVLR30T$>(o0Ejw09qyD9^6VDSyIw~RxdW-FjufE9ZfWwfk;^= z!$ZP4)TS;J=AgVd1h;SUJFgC^Ym5qp_n3^RboC<{fv{-%QL{)yb%0g;_h0s5VKpsfO=FftIBA2YPGK6V0dH7GX-8r-yleZb#kX*Z#XlZj*v{TpaL z_jrcmK-+6~Ny{%}DCg`O4VzLS)}_$!1mz#JVV+YWCir|XWe6=HxoZCST`NfeX>&$Z zt-_uf5niD33)==9uC|-%JIdGj8DTCjuE@Y9BQ$2cYc^kBu|ZMM-O~GT4xYPk0&3d% z0crGobw-9hO_P?MCu=Nh8`{tf7Tf(GuYV$E(TT2-F{tGQgNWO z9j>DT5qwOKk1>%=ROW0;m~~ZiH!#OJ6en3)0H#Ki;%$yF8Ks;IAspy8yZ}klYH2qt zd_Y4W@Z_%59CNPO#tc?~=`G^|P^uCHE6cWd0leY`5|u~#Ui4#svpQMLffTSzj-8&w zC#w}F{gPkZ9C&ls;cU$yPl)9X-O~3AnZ2Gj=4+V{U6uwH53UI10<~J-_`?Vnh(|`f z%fO0JM2UXX-`W~MwWQ399eD8GjpiEvp>X4QsBwN1#-jjI_ui)w86Ia; z#X-dH74(+fg>q0myUH+eAph&}H+$%-0Ao~RN_Jq!-Uvhw51v(B9*XG3fFg|FXMLBa zrn>&qAii2aoj`d3n1*>3Z{MV=**AePuih{P1h{a~Pbp2}i|F68cSq}V%e~3S#k`a$ z7@j4wAtfmm_7g|VDaXdu7FhV>usC+5q*#&ClE*xtLD15|NKI%Y$k0_vLH!o&b$@zi zZn@;<{^pV0dC zH##bJq8$rXhGoQX<2yJaBjHO+yGS*>2;w!?YT;(0+#|eL^RCojRgw+%)JSYd{KomK$l)2HtSm# z2Zsi_7{9Ojyd0}j7Tln8fckMTgf$0X)HcF4tP<-|c)*pPaHXQ$D2Ybb$exSINT{Qc zg;kz%_&-W!rqe8*2{L4>suOl^<|K4$h56ssfVkQLt27PR-+`9 zq7nsrDLhhuc|jG!LU3^z%{#~83|qeb^xmZJ47k7IIWHr$I8qve9k_cXVc#$x$c6oe zBR9`5?Pu};il6aHXChY(zN3dF^jnLbpQF}|p16ax+|ivs>FhJ_Ta{F1hCo?}P%qdk zqutcz|C?LXaY0^ce-+`W5rrHolTyZzkLrwe3j&WHyXb>9LW>&x?}l^gp&o&)cf%EW zTPAij|L{)y%5KH{tE_O3GHYdNp+P<5eMApDe_Ie~X(oBmG$o@tr@lrcpAJ!dBu+_s z!Pj%uls{}Di!QYbT-#*2rxM5e$f28O@+KUyLI>v&EoHNsKgK6^cXvvU`=+%|8-Fl4GFgsfuwrO&!mUA%ESx|SM|rtVb} zD4gV>isCiB8#X`tnC2EHVDtsh5VY<=rnsCC@o?|Vfru?Tpzz2Z8A?>*BAgJPxW=)L zX)YqjNp$)d&?AOG%m+&5K#yy6aB#iMWC1&%0mHomlcZln%DS+O1`@A(+_K_2@0mM< z9d7tj7AQA3-v{M`9b#$=wd+NQp7Nnth`P0u?SusrO1%?RDfaLDSWIfvig{K`5i^fr z1u!1idU-L>xl_NNQ?qEZmNC@*F6XFrHbRxV~ zDZv4YQZ7O`e6Zr2cUSdzX`y@K#ACOwk5@AAx68hJ`E|*JwWkrx#r3J+yhL6 ztVJ}b*rDO{pBrqwfAWu~vCM%6IqM7r=LtZnuuKhIXvs9FJ?+?f_DheAVSw(h>0?@Yqqm4thgq@?c&X$#*uTmz>f6` z+}|nZvmJ`q^!*L`O)kd#q7iBfLeM=_f?A~CZ~C+mrXRZohk7Mt;ULfMKXBYw`xCFt zXB#iG`)Wgk=TB$JUAL18^LwpFbY#F}I?wkk@qZ_}MuR&M3!Vn5Eo zKUoNLOYBHP%Sx8B`FC5GV12lr_?2m>#qaqSnbm65@eN?m>4hMp!YL z4lNjZfHUiHB)|j=RM5646_jwQj3{gkW1`9=@u$idXvXQ56>?WwcKoLG^Kz9X!)ClH zDy#o(y;@^}njv4{0`|70MVn-R1ck>_ex@br9we!iqvy{Xh1$c8M=~)Z>E*OaD}j~% zkyR>$cTFGeU4c7TWaz_vQ&`o>^x{9&{*Bx>h(<(4{8u`7Lr!n1?J@wv;u~GM{Oe%{ z_ndP+S?S^)5_BUQl`YT{FPy6T%PF2)To`1Ez*gYuCM?uUknnl2l0FL_ zt`^Vx@Fh#K{R9CZR0p2s`zdz%r}S|ws6Dbo%Cl2sy>JI~NMQxRH|*M6!wa%PmJmso z5<;K%LAR5RA7p)w&QD}L)8sVwE?6^`7`qTfiiWdN;9j|^fw>AY!1$5K zjj6$gE}2&yW1GqjY^}|6bUhFNihx1XRqX1DjA%s<4uB?q0wV%whC#YCiiX}7&V!;j zHa#6Ygr9AojX1(1LpbnPup4qI%ikj5f8qau$wZ>TkXIKXf`S!WV$+Vy4#p9gE^D4D z0Ua*`IyCOhizf76h+TcF|v9IzcAGx=M}=AyhE^e))WXjd&v>Miqh|6CUQD;bz8NOu^PL zVCKCOM^}4FnWYPyp>9C?c@`o~!tHA4WQDIB9q%FG?#}XJA+iijY0KY{CVak|Wbt8h zT*hy^xsjXs!PuM*9T z=EE7}xHv_C^_vgMdJL}>3;6k4o0FRpeU;uEhz45~CqQNvaYO zBLn9pPIbursr+R6igic6IGaY2_4~9G&1{Dul4=~wuca=EIZ`fu8(|D7W8T_kJQ$A# zzh$<;royCI4uiDaE&6-I5o4#7$I%22w|Uu#87ADyw)bs9UXF&VO`8f@DX-Rf`B&bD z)4EMyQ-DvLTwZ1G)s#dk*9s^;YoTK7t)hPG*?7mHNa?4MFs%TCM}{V>my;4Duy194 zQrw(3V3NY{CwYJ>V{1B!2{N)`ahL?=Ep|`gA6yo%O#LL+0V1ep6*oj1xGe z04&f=;qjMqkW#jR9VjwZ1Kg{_77EJm?)uX>R&Q*3J(XiQ3?aE^ZQYeoQop)&D6u-k zBlum7{VeGRQYNgy&%*V0UcZ4k-ao7_6HdLP6Ucrfsi21RXz2bQ$dys>z(%S06Uxwe zZAWbdTsj`dBMgu_WaH8gOd$KobjitJy_27lbtbDoETo??u{wUPYzFlHCHE`)oFX(y zdldba(qN90#2${wLbONV0c#fp?zR(_J02fg6KC&T>MF{H9WxjFkwQ>>KNc#R=P|DJ z_%i8E%f}2O;oC@b7~3j_j1E3+f6oEj96ZW>=*)14Ez*JIqQcZKErR8i)fz2>DTQ9t zDj4-L&=rS&(!DYZNrz+FNn|H!>UPs{Q@=2~sh+5zegV6KtB@MY$Hn~wN9#sL;uQkh+#7 zL-zjg&{Sq35S(Ek;oj@k`V+3R6S1CM$v=qy>=lYA-gG}oih_#a&HxFsV6XD_?H3w#x}=c6w3S6nTjdtcRTiJ8Z=>3u3oSMMj~8zJ!d= zu+da#)*8%*;fA2leaemCWUx!{(XBYa{BNN*1EM$v=~z5p5^FY-rlFvM5b&hZ>Bk|n z?apo9&SWKuhPK`U%Y`b_MU5o^uBr>=b#GE(m=maNtYWMf;Fo?3_LV0`PyFPQGEjoYp{ zW<5&{3QiG`2)}4D$UKDAL4?OK=BS0D(UgREX;RZYC=`H2p$%?2hLkys4>dQFr3TEw z?gdhyEPgg)-~4U*h}hB6gK)+}y?~2!pRk&rr~kX!h|S^V!FpFy+su)#EYtu{7KEX| z?t&a|7P_85t@*wq1|2!vd?D(o(-qNiC{-oaRTi5LlM)=I z8}jq7$@JzG6P0Jf1Y6xahb>>4XcvK&i70j@lrkg3nP6e|+nx)f;0A2VK|vd|PBWZ2 zK-W3&gMFA2US_@2wW)@S0i&*pd2$a98Pv4j@A3*jjQO+Z z2|^^|3-i6I+Etx!M9}c1<(p#;)RO~`F;~yEmXXvXX;)kyIxkJWYA8NuqB4rbUh-BK>fZ z>rLE)k1|lwO|Xrwz%Tq;$=sIchnq$(7REuf_y8spo&4hC1w&o;H`q-q7o^BBnnB5p ztb>?Wm8b_#-54eC;kR}v$FXgV{rbi4J{Xk9u-DDW#y_wvX~dQkw%fY(o~R9$33{yF zKpu#_Z4!n%pRSCyvnzP{BUwNmG{(lp`j5rtzPniH6IUiK|@KvXO!JB8iA7)7e(#=Vkw(V=5#JA ze93}CLT8t(m=J|js+?QSg=t02OFX-*mtB0_2E;KmsVfglx(6cJIgkYt=2N)f4_36F zix%rG8=6i-z>a^JlGi*j5M9`Y8D=aUMVRJ-Q z?WY=p^Mpl=q-<#&f}pk%fkFKDf4l(Hnb33BfCVSl<)Vups-FeKN4Q3bh9D?SZQ!GM zN(%x13Na|`S;YsHK?aR@$*s=s*%-}xnaCST2VVs~X%0u$QmVEp)3 zCu3$8-@X*tkoc{ab(C}MeK%iUuk(w86wtXu{<%RC$FPxc+C zj=SZv2Md+48c<$ZM%kPNpN9I$a+^mBZ72z6vmGyTk zD9mXxOfR%3SFgI!w=qdUPsv@N9**#!1~Hj5W-1=Ral>|5%+b5SMxo{*Ep8hwAPh&GOgtEcxVZo?GtxMTQtABUspchm$9<1Q?)yH$=(c0oTN7 z@>abL7^@y~v!XvWRso475h$fKmswty6LV$h7n1s~*M(|l1j0=; zE<5&q)Jn0T4vv?lB@{;L$Yk)6LX;)TOwyP(?21m{SM(-~G;d{hbNkB0l?i}58BbUl ztYwf#E+vNN(b(E~$Qko%3^tc1(enLC+@_B!WvD+G)glrsag|+e>&1wO3H3u;!#^Mm zTZD}bW^CZ2lPP%U_=+yktEY+EdiVr0Dd&B`HC>g|fB?G;Q_|0X$wOpXInyTgm@#ZMgs*#+z=}n%8T={&|jXMYu7^OzLOOmpj3u zyQF+@Vh=V!V?C{TEm*uKn4tNK{#clZd?A>W8yV5Rb3Hdr!6*mB zBVgRWM@qokIaklo@dZQVyfl42Q9Byys(}agp|mWi>F%Wi!|6joR~VM{6Gj~PmmFGJ zqphF0H_>?A;Etw#U$B3Z^FAyfxKA=+F)tKH+w+8aDrAZ-`moJ-4ZD5Prg=kxdxTGs zQQC-j+RfSX#}@Kw;Y_dC5CTMB2KwzDmVfQ$zw|c7PM4_Tzn^_p&1Ue>Ay@rf_^}`y zjbhXGg=^!IqOR!}(;L{cYu?AfmE_^|un$>yhh%cEK_887QNlQMs<_@DRHpr@abgij z;}uj^pOas^aAy`LgJG1L`1IOP(0pr|nJsZ&;>AlpC~f?yaaq2LEsW z;lK$>2|<}wWi;p~G5w4B>x}gh>t&4tt-$0Y;Ey6}Z``Y(9&wm>ucX73Z`?}JT(Q(d zRLP`FGzzQlWwKv~3k9eQ6^!xUHGSiXJ;i6eYn2q``yVhg4ZV%)4s+-lwI^EzepF*l zk?@DYb|TI!sysY397mlxYrBd{SrSFT6cl5ku$Tlq&K9lmti%EnO}&FBVYy#?ATnhAlom%o<|_`Q`&O@4 zaa7>sNN=9obpNLup^_Gk@gpVKl$^3d9O9qPp7^M4=pIJI@_qQlxdt7{vI27}u_I<^r2xE{YL!Hi7m> z`A-gwywmI1Bz_H%1v@GpvrWcxf6>{pf}e^QlqT4_uyY+IJkIlu>4brba>9{CB8v=g z)wq||5bBrqM_BfKQ`X9~Gu1F`($;Gs-VQbY?Oam?thKj^+zeEE590QKn?D2p0>Zpn znXuJOhn)3-m&18{;D8~;#JyzKql7(Tk9XC*_ILd)mx7oYBT5!E#(2VyESlF6MlX!A zV1M1^>7A0}>=ckprBE<(+p{{S%8)qjO-#BCU+kV#>2HV%aIhp^WIys1&#G&v3`!i; z*DvV*a8Va}pEA!hf%h0y#T+2Pz4)&i$f15oCJWxp4^#&;U3us}9_)=rE->Hfz_hqF*pnDEV`W~;Ycxel1l;fk%U&zN_o_u5T8vn-Q|DgmH=2dE z;FC0oK@BI@?LaYUgXV*O%Wo@hd4*uEX9Y8+Z!`foFfTU5e^zgWoR!sF(iRyW!6^$L zd;U$=Q^f%U85lu4423hm;mPlZtC<&qhZO~GFmf{Nl`TaH7n^uQzo~O*OcPevG2NSo z@y{yA^O!C0hYbnAEuehZUeCN=3WEGzCRq1a$Q)-A4mAFGgZ-T@H|Q?9DsZG{97$AB zQ6Wczv={OM{Qqb`%#K^%4W*>~_@|MFR{!J>%6rHg&yyogH1i^t~gzoPo`(|0si z_-=7jHG$5;k|50FSNz#^y5r8`Y+KCEeA-BBg2snR}p97mNbiSt%A+b8NjqUWiXnnaGG242_OM8A3tsw8w& zQ#6-h*Z}}y7ULYnrR?!Zmevp>K^;!CWrKX@`4AR z2Yzs^ni!-5Cc$m$LAm3GxsE(54>sVc2O|;(q8EQMY{{A{_60GJ<)>a%2c}#d`kZ_8 zp#C3Qk=2qlRO(iQC{!z>(hSYMB0e(y&zg$Wlpkp%FRwU(auQp3=doZ)<$tvn`jn|k z^l506BE`pBm0?Gf1>>MHvMs*ec&(#!C8jg$Gsqm%_RO|KZ}L#9#|erCN_{r) z92d+cPub@ODI^1HLvsCzlDC18P^F6_&+UqY*k_yYd0a;kAi@ZwPz=&@=RK=-Cyq^r z7r2ZS#r?8HejM@Qq_HD87u(xlu7VIY&|io1eUiDpQ4LM z`-cR@g$K&GwLsF(dd1IudO&8RVs(ve2ohUkQ7zV+X<0tF5Zih`I%b}F9C@#W%g>L9 z9ngg5Ry3^K7RktNHLd0<3$p-gQOEIeG4lQe>B;wIuj-@A)R^svK;!Gcl6>{!hHGJe zLp3k3Lfc7DqJ0Yt9%TP@GBDVluP>ZG;4|;gUtR^hxjljVr?C=KE~_=+dmU-Gg@9+s zOY@wRR5ZArehC-p`j}l^qUg6SZo+lT=Se5sK0;}uAxP7U3vkUx84{x`eyRsY6kOxJ zZiwN3oK2J8C#U|gD-pZbrL^0fj0vim{--R0?6UB+=1avj zOCbI4wAg~XQNVf!*mfoKV=mp|k*nZuFRu=jswGR3e4&)s*lk4{)9Mie(CR~}H$TEO zwExYeqdo~9@E_5E9G=o5rPBZavdDuE?a^14Fozj$R2+2s7wrt+Fg*|OU*A?bJ7;F@ zo#==ly7#tX{lh=?E9t@J{~ev~&Nh+!91-r~GkmBMJzD!pIiLX{z6oxCgu=z0bn%JE z@pDnBns8viP#PPc(-n^S_h({Ga3OE`i&LhcwyUC>7rml#75P31$F8+WT2SKKqtgnl zGBbxGBT+h&ztyH=!hy=u6=6eBo^NuT0CV=99+A9Tmc^|*f0xuEEsSG$;b%B(*{9lH zQa8w7(~bJOPxEs-7;nb@b}F{6Pgw_fu&IY-pbSP5$H?4}N9f?MaI#E>3H(0@Q4Vv%UpVCqpF{TiBE(XTA|?Hiy#AA%sxRW(^(ODBlDTmaqAges1X30#(7*QC5K znOKCWM5`^Xzq|l)=f-o#OCpKP`sFMg{vuSPP=6B z7}}SnM#$>MxFL+6IONkEgO>D_PGZqzM)XYT6M2d{40NA1GRbuRt7Zt&kSY}t=Qkj; zupJrDQ?$H4hP$gDIW7T}_9<$_W%cBZJZ-&oFHQBdPqF|Fb)OL-R$1=*-kKeY%aV^;>8$ZOeKBD zy}do%+XM!S)?*>KWC-B&mllEzdtyCicl8lys1AD@WRD$XkHuts72y%)&d|ndG!0cW3ooYANRMxUf~7 zsPG(KRy&*hMzZI3)cz0yy%VXa#OikrA9eBz_CvET#$$J48=wm#i9Ior@&+5bL@BnO zM!n-`blWSzl_N7v=)at36i=LjxXl1nN``QuiYMwK6KosNz1v_$!*dM}9M7q&jiR&x zk}mJcRsVi8m16L|8qPc0Zs3+jcj{2fso!0X@M0=9p&83F`L5&0Sb9uce_4s%7n0$B zNH8WXeeH~YxJ}{`<$uM=S}|aTWuE}o%&6drbgmDT?mZM9o2G=>0hgap9XtSI{)<{7 z23SD$SjdNhWsQbE1_lO924QZ|>2~Nc$`9LLfb9uE{(c1wTXz(uuS-RV_Pu3lncLc6&^Bs zKP8=g%~SdacMb5Jtaed{TSv*V+-hgG*!6?P!KuB^s79riv5#UYV%m!^wObk)dSC8t_|$V(g2YmUd( z-{>_cPArvqQlATr0Eg~`$eq?hG^YGdwt?MHNUu+3f|E5D-Z|Z^Xs%{ zVnVa~Kcr8zmhOC)qYa z1(xISmpEBHpyou~Mc+Ivq2 zgcsICWnb6J^4DjH@TzxI^V3Nojq3Yw+VC3m#WPFzQews5RVuXQ{nFC)TU=T6x7Iha z_)LS@ZhFzwU}yK5bVVFZT0Q9fE2O7)lgb z(oeR?V=pxDSuFFghAwEx4W|ga9yGDec?|Qxd0T|N@!Lap6=pej^leAo>S}vzx}OcL zhDr6o1a1!BYm3rngM=l>IH3iTPYbaixKJ4k%7PaO{aIqqcXxN~w?{lt9*NQj`Br{^ zf5(075fH?QQS%DK@%7k&B8Jlg*xcpwHMejfEL(!s=Bg!iW!4Z#rKD4ie?VG3mdL7- zox8f*$NdgcDd7QnWq-nr2gYQ+%G1IYW2bV#&l$;+zQmW>gY(Za_nHoU`l|#q_oe#& z?V2)*l^3!ru2kXl6CfArsa-Wok^;vGQ5Ua$S?-w2VNKnhaaDGamawr%2OD`i8!3~Q z?AA}F{XXx1LdCTC--hW%pILUHXTB^jR26A?zkLBHa=%4+WCe~>j;1&>8>K_FJD}}g z9p&jtC{jIG*L+!1#+$j;_}#R&(gsAX7L31R`!%1!NDX6Dg%lFGx*&N)weid|Qbc@r^Sre)AkbL<4<`@U;tNqfOh@hkLAj)@4rq*K}*!P z=D)hc+jcJK{Cqq>T;UF6+s~@v?7!tlc4Yi85pC{g>hH(0VzM0>LSU8Q?vMjZsWU>x~?=K%q}q|6Af( zo*C!Fk9D@A*U;{pB^b>qGJW=92`ZcgMAcR4NwD z7-F+Hrul$*Hl~QxyrFKrA?hAHadIIs8hxIpAfA_#uf~Eh0GJHc?8vP-v(n-b4H3(Q zB6x^DZG`{rHa!uH=n56qj1!CzKx1ZNxA*@y>o|^pSMPK|*9#&6fQ-Y-HtTf?!7^5n zNYeOXSNYVZ1n?|ol!H!zzOTYv-wSpWxpk@$nVYRq^m>0VtfR9YGK_@%f+<+l^l3el zSbr|eH@r$Pmne`p>5`pcsmkkVU|{$-K`^u0Si>!2CJnt0lNKUB#e_e4%M%B5wWjh4 zS?gtuhM})!l*g3Uh(F z4tD!CR6J1V#*J{b{6qo9xK{GXwNZF>s!kn+e^?KBf;#fpdCu{AB9Pc~!5D*=giVR{ zL*+Co=!uXYw2|(6hP%U<3=LXQuJ^U5T3f+*Te_N@&NnufC$;Dv@0f&A{Dcig!{2ND z+Dq-b6Vm(?Vk3idpH~KYKfuDw(a$K2XF#3uj<%1GcFEv7h*@C6=duoWzXNbSzreTo zpl{l$JQR?EGp*U$0g+JMZR-i{c8-qNcK!d!AN^W7`Z3zs?d_g4fX%4u>y~y7lkC@3 zqwXR&&H4MRZ4AR8Zp07Kx&X@KpS&v7eoEOJBhrHg9L8|?(Ki0V2nCs}kxVw16aU22 zY3a7^0)9MzQgNd&Uo_hOAN)Ka+|l$I<|hB}aY>Y@LD~BRPLPo1o{oxk@(&Bn^a#`s z4}e@;LRBu-&XQ4uS@!+S6*9)Y0DY(D{#pCX(~Dy8?%PC&fQyM-B|xvn7n`DF&HC}9 zV*Du+J8!rI{CnO*)E*gqtG1#)>TmnJ``nc=C-Asto|j=!B@-^Bi7$8Zt9dX}2Ds{P zo_ac#jmAgyXO5kJ%$>RYsswnvnMqF@&;#6_5-8X@SOC+{V~SxIo8blS`Ka{09OA-zVy8({QkLF09JFB%(+H;ErvuZAo@S6w{zG()}}< z?^9bf%GLE>Rw89r zjgVRf_dsrkjMrW()%Xuo5}F9q++5h5pdpg>YR-OHrGqZatQAa3^ku=yXyO>5hPrfp z?)w!1wnjE?o+`dQ@zH_sA|qjMtrY#FMRO9m4I@}shF)t~1k^^~!Hf?+$BrC){6Ge} zv?+HTW*|#Uueak5psH?>Se@C8*S%kO0t-p{8QSs_Wef4(L5{PtvqeCULR?xJt^R)Q z)4GWXCm1Nq)&GV0C-#!Fxdwnp(6$fa7?Ic4op09q)3GN{A#fLO`Vnw_+bSC>e>J@s zWl?NWkGbI|2HV;re}wi@oEV>=-Dc#$XF}39oVJ9s%s`~-Hsd}81s%8m6;gP`FmQgP0K@9{WF77BS$*A$M_ z;rJI4N{b8bOs#7Q+ZT?yaSJ>!ATihgjQNg~%n#>+0p9r1v1o1C{vx zz4c%?sl3i_b2TYR`DMbIr^~{8f~%MbQF7y;p7aRGlakxqrdX2G-pzX@pfToWHdY?s zm{gEH*l8v_r@$_?4sPd$okZra`JgjodBXs8vKoQ6G|sAGLX!*P!UH6JIT3*TAE37R zkRG?b5fF|&9MNq{{rBEVSUfs(83l)M<1T$<#FVkqrmNxr{=UljW89^TkwGZ>QJfaA z5cg{q{2YKi=2KTh*LOX@7;);LAA;UWMDf5eIMIg0=KME|#*tSVV0-f|Q}XqPJ|lk( zmiaDp*Gg%@3zCeOv3JQ|!OU>9BzP6&2ZX8{6$TrN1@5vLrxCWX=-tzk)Oef4a*)Nee;+fbV~4D8>nj=@RW3aslyX8y zy5UtW!U5UZVbNwX`VA<0KXw2;XB&g1zLfQF_&LEPGlv>)SQdQf(7Pe{3fAm_t5^Ba zT{dbXl;%fydEf|WA9ucdFdl=mmq!8PbMcdF1Z-b42u95LxA>()ty_WKPpvE6>A)uq z-!KypUsNJEF=t7qeU>vG;`fFT4Vtiqy5UwttCbx>FLf`q_oYCj71obHbV5Cb$#Di9 zNGz8-Q()8i|5$+8EFJDc7cl|Ta%$i?Zm4|*CI7naa#HL)h3QT%3@QK(+rNC1?=4O zLGH9CC)+eiPj3WwTz8YilJ|sXj%vNf?~!PpQU3XC^kW_&)XNir0ODFmcHVdy%rsfr z8%4~Cp%J9k#Y`41uZ$^-&Ra2d(SLJ%r0gT$_9mc5XcbnuKIoD}92oAdDfzoD5QI*n zj9OL$&QZU=Loe3CYc2P6j*a@gj`ei&DEhQ`TFQE8^ScOJ^ZtYx#5FXP98(YTo)T?g zIZpxM%j3yttpnz|&BuyT#l2$4D^xyBK`kap*7r&l{FwSbY%SD(SSqn#gM^NTu9?Fu zF}k+ergfK&z*}%DAO+K?2i+o~$QQ(17Z6nfP#e2Ls)JSnQj4KHWa*CoL2Pbh3)KmI z$6z{Vy;g(>RMY8}XZhFaMB)^5@kJGzevRSyiqx47R{OXJ~(Uu zas66K7O+cdsCFwVq|2%!DW}|rBk+CQRsP(`SDFwv_wg54Y})9M+oyX1Q&mYGeN}a7 zeH-u7@!y-iganB#I?Qm-wneE%R@AP4GahXk`0JinBrk~JqE1O4L8E=B5&eC%+7-!%VYZR0b?*r)hYgIZeBSr0hMUx(k8dcJ_br!Qzs#<1E5Doh z<@}F6mq^=lvQrHOUCOEQzCoAM6cc)L`#MWi3oT5LK2^I}>W0KLqfX5K+%*Hxv-xE* zaZ_h2Tz=?caKcDb;R1aSJv0;kJ56HFz^InD`{(T-hg(4xKlSvY+uv`?|6=V?EX=t{ zOW!-DjO?JZ6K=CHwaTT1%yS9Z>JP!ZG&9|2!~Zf=r>)IKLxze;R*ME|RA7IEWOZGq ziZ9tX3|du!>*Wa2v|5e7v*AuA3wsj_C%8l6xG;w^WpjMVD(Y2NeTQ&Rw!?gy#Y19` z)mG7U$z}BVL0+^@{B)yiprn~`e*FuhL^-kRj8F=TkS$Hf)NIA zad6<6fw9wdgXyuC506}jLj6mj?Rs=d!7MSMaDO zDzgzfa&7Izc}4Ks@-f81jQ<9KqNtJY1WhD8qh289@b{TXc$O_^Y zPh|79%%LQ(TO2Y{mi!>{Xo;4M{DYE><@aff53$E7-m5a?i^Hu3ISVf;H4R_qQCb$i z!)U@K&9pT`8NCg_1=6JGxc~vcrHmUzf;|U~yX- zx$ixguFX)v^g=MROMZd;xIRXZ&6=v}ef^R#`UL4T|G}|(2R7LVg z#9hV2dG2_wO_{~`K7G4R5IzUfYZ~f|h^5YnfGI?9?(4hdh>e4Oa^V1TJ(vZGwmyaB z_=`}2={=U|J@x55FNr~WFD?W`EYJ(ZwO9vE9wmf6HY*FIglp$z9l@=z0L5Q)vnzfhqcrdgrGf*S@^}4Xz-!jGBI(Yeso8*ur z;A)D3x5A#?#=BQ{cff2Qt*}7HPfFUGhxk2vs0whY2VwUVHD_sNKn!#p@^^g_0c?)Y zg*s|M9{UC`XjMybF8AjdNXs+&e|wJkkD>g!Y3d%Z1^|Cr2Vi-kpai14X~A`vF;_Jt z)bxC3KX;DBty|hrT|#r%m*8^@v=&MT!U1}f`%#m@s^0V7lw3>(K!hZ=+6g90C~H(( zQitn5Vjpg>+gmlcqd;Wd{uE9hlcRLi zOo-(?972&OnJpUn{#Lsqo1(ivUE}rA@PI`V8kdMS(JEY#(^1_}(P>PaGJXUq_SmWl zM(XS)5@U~|OQz1>K+du^=sPjXX`;!ioW<=oXU6jS9J2`tSJX-2CMnA= zv{2UcQgsbG|5?Xy%`A5j5!v}GSl|BODqNurtbPKT)q#yXLiiY*yEH_Ocm_@wCu!=6 zEh$q<6zX>a^k>qWtZ2hfXXT^c?SY>{=R521%t4roYX00_&)ho!FjaASfc~KMLKX2* zlM>Mc>)1ZA!>HLi9V$2#bXD`rSHc#>h+z9h0pWO2u!YCB{6C=^iAI~hr)x0wB4x?U z-qVCoa;>F&O5PC?+`W<*v_BLRbb-bz+xN>|U61*A1QwOtnAb+J5XqUHGfm^lTpuJW zj!uu{D(kniD(Jg<%ZIlg6h3oRg~@w*(jxa)K2A~)@pCBg-YmXl)7@}&7d7VW9vAWG zZ}kkvNn&;CIXL^P(EaLW%8Z#QXPiG_8qoT;?4Kf$Vdp&E%j-@ zC)AD8;qwcvqN7AhHt$;9!esird98zxJG@`~@7JA(Eu4(aj=@A4Gpl10>DEW@@;n-C z1;^`HvRmLLNqm(=rPsXn31KN|)Is~Hi;o|$nd&1^j04cg7UN)(Je%Aon*Wmmmqhn%RQ=@ISHJgb*InI9?aqCiN z`n^y!fZ)vwN>A^wXWABBaZDfY$?xcd0#oc(GQ4xbV4-uqLZW^jDHMDyJovVY&xaR{ zmwliVF%4{CvX&reVi#b>mr7SUO+Wg#d0T#b+CfR7=IUyk(N&h!SJn~u%BqwfnV9}L zxMN(rQ=3LqB2oaCvKWstgwfza5fCQE+>)%6L4`Gf0qLpOJYz^TkH-T~yi)N~gODjs zv8k}+-Btg}7cxa7g$i2|olZ>|#6)DSM;C(UNgXNR88`G3U>7p4j}Cuy%-VWo$lLY6 ziP}=8W-@=a%(kY+NT1O}xW9SIkEI+1V&WzJKmqRig^u{Zu(^P&BRdWgU;3Zxk-9(u;o& zm069sh!T<9C~F#4xeGa*8-?gQk?wPJm5M-iy8es#Pa#-I<_GuF*v-5D^;Z)2 znl?J97+xCfqeH-B z6!GQ(MLs&T^J=K?ZRwG1=^&Lmn`(|>0YYrWGdMT0@EKs@XG%E(@rCfp2<#<6c$tt7$a+ErEsI;F*^wnEEM; zdr-meVKtDox2?71xx(E~StusM35N)*1eFnFCC}%lqCjhHgKqL5&-ASBh5RYe_e&B9gA!^bB5UFhSK@CpYP9Z+1FK<2HE{!job;(9SIGh;H$g$)O-(MV zNQl@MRKS(s^3;hZ-^(Xn(QiYR_#1z}yRIDHVgXUz@zXMp1he`FHO zFkau^_qHx+4l+YLp;&kU&%2B{pY3}cQ7nUu&EHtq2{F_@cYb07Zn!Vc%n&e{P}2W} ziIDfX)8}bzt3aSulcu*v*u>0SZ$&4YAd-V*24T{;RRzbi-XC##DcF-7y4O zyg^cJ-WG51tW4G99&3^SfMa0SlhEC1Q-wMbCSc8L$i7e7*?_h4V-5DVzA+C_JK&Nl<0wA^oByMHAG~Fxk}&afSr^?Zp>+ zoFtO+1fGbVAn6SH4wqyUv95VONz0L-?)hq{fB4?V)n(;DF7c@}VI=|`-! z&$=O(J%fy2hj`ylat5pYQ)I8P&)~w*Jid^ZvOaO@0)C8enwX9t1s7#V6wbS0!3ka7 z$osP6SkHU)s3*oFlz;4}w?n0A-#Ynqb-mjKbE7$_A2Mdg6Xd}Tq1IT0q_yDVqb7K| zCbn$$zKNqbfdgOlLQuxMwpK8>vcX+kIK5u)$zjQWNC4IF43YI&r1mypfQ#1jfB-M= zdJX9}I!j1Kz*)~?4FH%?3O|y>y)F6=Q9cmVTQjyCKQglO|H;y(3*g&$E#HW=@(cnx zSBQGH4@^R@MRsDg!KlOyvXY;)FG%yl+yPj59d!|G1vFsgB3w^-+TW2$N| zAREU#r}eqQn(NU0c{B#XSM`{;;09DBwoWxYbIk+_k43GHix)0h4-t5~Ax@UIjz+u* z$HY8oN`)9VTbg=@4olcXBHiTdW>53T1xoyK3&yV$0viKU8hEhTGB$4nCq6H4dymEn zshvo|XHkS_4~ZVuR+M#TDlF*$n}!6S=~5-*T$#4)#yq4DkI}yX>ldJ8xsq&e`SR?T zXz=l!@4$~?yyNG0J@9k*FZ&{fbgM~ltN%J)tu*2in2hPRi(T^%w!w=}1B1biy-)?O zeBnsv@e&vtr`{p5Aqi8-+w-b|()ubT^ZHNYcIWgvcBJ++I(osws_T(<_p+9cOL_Nz+X?rUi-=+*+6OOYe50*%ja-;V`uAIkl=XX&VqthTqR7b&zpWY1!Odl@fuiEf z0l?)8!Pnv+nTxZ8{xV0spd~IseelC&tZkL7#IKU`#4~e&X(Mn>l+9qvxr+~173lvS z84CZ|o`kGo?cCdgit~z9AyKQB&rOG2%$I(>MO9$Fd_s!%@E(*BUrRR#awK|Am|sM) zkq`J+5)rCOM(gWeq4J)M=y~NELnDvFW^FO{&kw(D(%{c;&1?Bwf4L=vj;PNHn*?6p zFF|07*%c85{*n!4Ge76P3HycD811bcyW7}A&q~=g<@5~Yv-*>m>YSR6+q2&xPg7k- zTi*hQP+LK89?LL??ooE&W&+~GwxF zZJ!co=LxdjavAD;DrEE>Kk6xy4p4tU^H|!8_C_t^{%)vx1#QQ%L3m3TcLLG`$s7nT z5Zv1VZ^d}>V??h1<8s)o@Lzd1T-eDAc(wMV}Xq+m5XOJHT9^zGKhD@Q`Y(kC@2m8Km- zD&Y`_7Op^)yDWs)7HO#$#Q7&1)&Zj^gq2O6d*TlqplftjBw1{}WO?8@da1{iU|*g$ zoR_Wn{bY8U&_+Y(WEZ)()BuP3kRwTcCNsE2E2m!q>suUJ$j3%wEpor&T&hn0dLLcC zJTBJa21D>gsOI;M?reCLQ5t+9k6}ITR?Di^?k()g;GtkADAIMcX*Uwrqj9O>dG5tH z_SZ@nm=~j#rScAAhewjRAd8u@30HK=sBQxEN15FC0laJijm|sy6IqvG%3Jd|Y79v5 z>NZCFc?0^0V`dHpc?aB@kRTp?ktu`1O z53nbSQmrVC+{E8v)18H;uzaFV-IFZj@M(gi5MTS|>Zu_mI+x}$TpRVLuL!;f<*R(+ z%_F4AS(RdIHvdW?Od;xWyH=WT_G*4a5yR<(5eijihGj4++ygcQQi42)Jrw_6Mw>_D zij@NmP5Hv<`(9q^FFM|Ywr3T4y(AXg49t&r^>F)(+Cb=wNEKE6!v&VN6|__cv6#iR zOk4xnw+cFQe=REB&jgzZ4&*sknuUqb^@IiYeJvA+i|CId^+qB*{x{X$hOX8lMil$VCsdK z2Cw+=tQx#^b4CA%RZsgz2W143BeCVBVo>>XX>V`mEB^s1%7?+|CDTtF(nZ!#;P7hMJl4G`cI# z9oGCD0bbkR9WoNYL-~nU%ngZgbY0D#tQQo?A6+Q-I&#TLDu_x;DyV#=8?|jUX~-cD zG1-EK|FMVeTp*X>*Yh#b{>4Lats~UuS*z#ntHc6VQ7m1$5h~(%-7r@EXfCcZXKGD1 z4TxJGzQWwcHj#*3L_HeCzloF|ifa+bZHsB^y`m=KO!@ddlwfyhbNyFV{eY8&epZ!G z`uX?JUAHKbY^H+n)E`dp1}L!0>%}CVfeV#LUs}iG2o98%?-C7H!cvs4x0`y2%&n8l zVT6tCGAg}#spXXp+Y?>ZF@KDs zTMSaSDqm1kLOM&GaJ3`dH4~exqg&1OUBsOB9y(^b)G0Qxeq^k;cex#rQalb0v9=A^ z)M0(+tJ$jf8~AT{Khs^{YkEHr`=tk|mo)0IHXh@LL+z67=YjCRJB_OBDZ8ph8~Ewc zeY-W>YzlpV^@`o8HGTd)OexEgFh&UFhvp(SL z5Eh6J3Y0s(B14rpu`ktt8bONupm?KRiO}(w2W&mu-%iB%+8Ge;9Qf%S>#8^PDBv!@Cys^#u+DCtaHImPW#?b6=gldB=!XU@R!L|o5UZV%Q7+XH18F*2YxoEHEl)NM zLO+FW^!TT&ZUUJvUXJ5pAy4n&?$(Xas%)k?v>C@F9>48$Z_Ahnw+PODDbanfX92Up zP=;Om+m4rWgc6bMG)YGu#r`KO4|)el*4RGmGdd_bujpQtYnHITA>5$9X%e&74*jK} z{UjvH@@AApE~*LC=CCymfiTn3`k<6T=a{Jf z#{ww5trxH~5Zu)Qcdw+$QZGlm_Nj^=pFst(+7QG6m*Ozq%#sr`=$)1}?emJ(mAmxp#!)10 zhl^lh;$a2o-Ris$c8kzQik-EGIaXToiE8$B%_b&*Gwt?rtUTHWz8G$};cO?VDD{I; zm_DG1W`E#G=ChOk_>)jrj&3XjU$_4F`Jz8>-4YF z_6nZFwNbTD;ohpv^-8j!^KH6i)j-{>Ho zShinx3G~=Y)$9jYYA1+=ILb5Xek{BvkDp<=a#V}zyLMpWZtAJGlE;=xq4St!O?b0- zo>P7v(2wGhWd%>@mRZ>$VD`?e2cnGqxM3=_YRyR#R~tk=y0qqW1;wrXm;x)jpEQF6 zaDqtT*WBCND~-kE76$udijaF1V~9FztEbd6-~j0v3<{YSG@tspHRSc}W>oUi2GGq` zskH34st3bf=Z#B}8E7jRo(=nT*koKkm9XS*#DBdc0>=T559%7 zk^PNkaAn3xu&ipA5%`N9I2z%La3tL)^FD?L6dIw z3nzJFG~7C&B(stjE|n(cumZ`j&zecM@b~IUfSz`_aF3)VP%g2prZ==u7>$UeufC2X zp7_{FbFA|*R1hafLpZHyxi2kd@#^(3yt6*%VbCXdQG3@hSRivw*&|372|uX6zEqtt ze=LV=Z8MqNpTzW1H=AnEpI>nYf!!?-NGR46VCx5iISj2`0%30IQef*J9 zaYi<^+-)Sz6VrmPjArsxEum|pXoj3CH!KXy#*#r5>THjw>Vq*gV}m@+hkn^S;HB$8(zaPoBKO@XaD}em2z&jj zxZ=54sD$0ocqm#a;F#f+Z>vLVQW+($UL-C&K!)2KPwp`_+EDXz=x9L57bCUX}>r8 z$Pr5+czHVrcRsHZkBbV<3Kf)}Q@J3QIAa}nA{d@ueSM|d!nV-t)yiM}#`Y~N^pGXm z>hbYv@GwtrHhulmPoueM^j*Vt%|QCp=UWY4gmcIe9aoiQW1&gHpVhO>KZ0~aMYtT^ z3UNrdgq}RBL4@3KKEnlzS}TSM)A%*^A#|z%$mk-b^p!sid6&oBoffnU8+_)!a~j3c z6kb%{wOSX5i4{HBa?@G?U0YfH{fmc@%OBkJTFpCK$~?+bJY5Toel#@w!=|lBDeKiIkC`_-6)-U(o1=?i0zZDd%jIw=e9e7mU-jFYdB_g9TkT!9NOsNy<+2j5McG`Sb8*bVfwZ#vw-u!(~~6P!21Cgm~##_=4GW7-mV zGA11B2(Ne(sK%5`=TA*lO3Xy-g{#Y9e)Q8s$n6W$JA%&o`?gNc<$y6qslPP`%S|d0 zOC9^@g=e!TEY5Xr zWCgS1ArsnN9XdizqH~w{Q4HtPak}zR$VmrQ7{cGaa-z@eM2ojx+t)SJYQ16~wo8j9 z(ik!XC7?>>&T!0lG8_lJN$={J9u7zR3+|gAr>hS3ya;t&uG6$5KNLKGFJy*)dV*|7 z{pE|Na#d`7^(W*tz8m|bVY+HI>-?DI51#SS`uh<~W!p4rB^-TB&e>jZO}BY5Y-9V0 zd{jx6-WFpZ;b?oRNct`M$&<7FYzH`{DttwczkKs^God8(a`TLyP=`UHdkGZFr6pJ2 zvnLvGV4Q2PziV|%gS{GBZo9)%ka6$voa9tH>8MN>?-NvAEi1LoLpv@hH!pS)Lc{z( zN>KOPT1cfB@38U??#}{`4lZ#sPB`9n~&*!VRao%!Usb)-L7(Y(Wf^8^P8tg zG!jv}qj9#^nK)gQbrL~_1@4#N?6Ri)o zY1r6K8mqBw+l_5Dw(X=b8{2jo+s00IY@7d`-#O=c-%qfw+4Ib-d)-*${VaF6kJD-$ z1F9WQn*v$lUoxZ+*jC?{&9iWv?2oI7c98xh#n#uytuj1+|BI2#BRyL1UeI;PXj3t1 z4{j_=HO~~{UMQI%@`07gJ;31Sz z-?ZL(hR<-jSjkf1>Zt{&)2e`?6X9RLsib3U8|cqtej1CA!&hhFax3M^T|lTc2%S?= z*L|Zy8S#kjv|k`SSyX+i5dWUI?8+Ej_wq6F9&qZk31;h!zCZ z3)X!#d*P1YFEJ-=Mn>+)88+f(&Y7z$w*+WAUSVAwPzpMH$0+ZtCORXEYgQt%F4LrE z{OFembiz0B^acf0Q?QLuBkUWrsbdaP;w<9d!S_^ zWXP4*8X8zqxV_wZ+4SqcE}B;CJG`G~j=~%5vGM%NkhT!$KVDj>k$R-xz%f?Q;cCps}a?P#`Wd0o!b4d@vJtft0OC^55x=^IOBxzx4f%BbXiUK<_m`CU&d0_Qefx3?$e zBCPS&6QYQ0aJAzAi_oR6ExS}tU#QTN*DhrnC-Yv;3E^pXO2MPUqw2#ZZyofKW5b6X+Q6afFY`I3TX7l6%-s2ri zQtnm*Nqgs0`#MI{>UeQusJhN$>Sa;asBuOfSA)@=kq6qD-n$xZcP-)fAPqgXDtf}A zX_nlHAjg9U_MD$w*AD})xGM2ixC5666hVHlo!J>u#q7~F1i$X$Ov>}{^jsG$V~*Bk z%Qjm)MFW{fHDkP|E)Ji~j<6xzduxwF6}?>GnI-gchZ>ZX*Xcl~7tX+-v;?NT5qJvM zs*}be^+?C#>3w+`ZEse2eoesXKnm~Pf9(eq`WCRUuot$O^$<_8GWm)S>WYWA;iT_i z5*@%Cz(Fhi7X$SF05t9Yw1l6N{18`BXkmk(cX97cn8VELX#Y14_oqZSGh0vEEVo?cL z29vx|9-{G}ZdHH-;rgIJtl^{~OAq#5nu_?g--_7Ktw2U+oDR$hK`(WLb<7m&(XXfuG zbsIr#Z%%kSku8a^%bj5_Ti{Swr#7(`(C%P&g@$N=>gWX^zfB$VMDh&P8U?SlxcfxML z%;zLQ8OV6~9&K*>X%5$ocy70eGmTPk}uslByp7{GLsjD^1N~+eo*x<{z-wy zQ5X#yNJfme!S0WL0zB1iY1UQRQ>a*Hg+#cJY>>RQc;{hg#ZQI%LXKJV#L1G4Y7cJ7 z2>LfF2iRjX&hfyO%^`b%Sy7l?hzA@WxpdD~hxm!L@-s!JH9CgP!yoe6Hix$SrryEk zhit1V`5X^3(}v?2$40)B*Y3G$qgyYcM!$>phuh%P8LrT#_H6#~kg1pAsoEQLKa&7F zgTvCFDd0fyUzYeLBKXmjoRuzl58ELh7Q&uGy%GDr)o+JoOS0JL2IIX9PL@^*~{aC040=&t8afe=5^K60CLI8C*C>VH0tV& zM2e%p%{An~uJqH!Dc9<-I7D#LFb~h1JYe)bqi9-46mU(){6X%LP-ID<{Jn{>^rJFh z!fS9TSabi-io4ct_{7PxTm7_xMkl}C3AA#AYLB|v?ApoiP62%O(~-igGnJvQ_N1}v znO9Rh0k@YS#d-|K$SJV>{=+X;T^n1MG4Zxee!0K*$$(W9C9R&lpYHX8QZ*HYYJ+EKW;{CX~IcqQ#y ziCod3;F!3O^7&dMR{fz3h+S64%yw@TU1j6B+50BoAdP8~jiEls>;;L1OK!ubg z>@0Uon-Y6;uy-JmU~r(ErMiN$UJ*u?42gPf&L3Lyi8L`8<8-ynKiS1&}?X*qs#Kn;igfnl#_@wdzk`Kwy* zT(kO65pFWmv(?73)YMdzPF+HLY-}pmDj=r#R+#_@i>nXV>_>KBeYNJxMs#1oU##T1&Jcg`My|we>?^u zi4{UkYYk83%G6S`*KnH1{-y4JAV>=FCRd|A;#(?4S3x zZGS6Y4vAmblsQKS36cf&ZW-f%FFZjcW7#$18FJJ1x^^;6r$B=-tWBQx7{9N*q7ZtLRn_8v0^TBa29X zd1)>hteM~t@BWIV($QL(mYgfiHIDUJDUPlg8Fs(OVb83;M<{&F&I|m*Nx)}{$&qHn zOE#y<>2`4{#$DGA8rZ^Ok&_g-z zCMG6ipfPMdQc@9PZ)TBf@PLTdezxAPeG|r|=u3-uCIXn|TkQUqDsVqVxc$H|CxQ={aJ2SyIp(Qr`h zF%CINrht*O=N{ZJOS#h(p&BwUY028^o8B{PDVDjR=&mf6S~cSO5ae>#2pi?^=D;{p ztEwEf)1W^St@nZ*F($(w=drVe5U~WerVep5ZXtCq23(MQxAyI-cJ6^rZj7RnL zGa$cqS=Tq*ifuvg_|<-l!^lLxPdgmM?RfT=3PQZu>Zj}X26SI=p8Fjn`j_9!Z0 zaz$Yv+s4c$x@?u>l9HpthZZpUsNu!;d0IzyZ^*x~&tp)Wte3$qYG_~f*Qrbsg&1@nsiLseyl!bu*qYkTgwKE` zu`p!8eAu#{K5v5!1|O(X_&SxG?^|ON?3W_{hJ3L?Jnzowu#awt3A7WtU-xII$Y^ zk90V#vOSV2rZt11P9EsJu@^{mi7&hsFZ?fhgRH7oxPx*Uxf^ZIa=)hrVa4|oXXvLZ ziQvtkn%UFa?_teu+ofo7w*4NsIb8MHKF_V&QUOu0KK5zYT@a5e$O8h_@xVo#Gw4>;8zUh5d5T$qboRJj0bg|ArIEd#@enS+T;VtxEe=iZn``mcEQrVD9~HyYKenQH ztajw^EBqC8-tK+)$FTY2=cK*@1&jK1>7UM0*#UEPSxIJ6olnS`>FxiL!U~jh1{*R6 zF=fmio;+5wlsq>InJI4ZejFVaA3kt=xDNKJcn1p@z}3dT&DJF_CaoAAF*W3O{+_#f zuGZ@+IG~ji3{euB&ho1i*9=_JN*r;2h71(6A4f5|qn@qTz-OS&l1+=U2+`@EJE&P2;k1IyNg&mH*Gg~408$m>l%H!u^D__;O(*jO0iFP z1d9}PQ>V~7gW6-fIo9>o2Xf?5!R9RD9^b-;4srgtl)9qSaU|FY%4?UaR-$_zYGko3 ziFLfZSJ4)4{i-sPPW(4+K;VuyIKl-@)ukgrjDU?eXyY2+5FB1~#J(WMiYi!!y4W>$7|kmoMabm{qJo_py)n+|ut3h}So8G_gdra}D@ z;5^FK%z}5uYE2^xeI{S+FU)n=(hfc<5XSv{*ssqM?+A`JZn9<*pEEOsL_zD*f@9O^ zxNaN!!%tS9EEDj6boW^7oDjKmbQfsX=Ip@tr?{9Lc7wAe26fgW9Y-W{gt~aLgRFqSQa%~nNzq}4{WzWfD zNA4267l?*X7HT_rIewFhA<8?u+%iNf^ZVa>PDq5`#wRMW9T<96E+MuJ;{188*<7N} zJsC*COp-9-Xy2Q{UVUO6Y%!>A7`X0d5tZM7;sC_#lGOqep`lK?FDwunGk(2VeqW>iHwobwu-%=vi`Zqj2~k8 z%C(#5Q?k-%pE+vFz}a_Vp{aa>EFUTQ;;V3Aw|Qo!|!P6K8BRLGyXLpYbrWRfil5Y9ib5NYku4F19vfQ8k8A0Mml3QzyqeXcdkxtQEAla-IL z?-ZmmS*?<^Yd4Fxd0E`v)Y4}sb?AetcogWYAzfaW-o>QFt0STg2qtFeKn-Ewxj^7^ z4#w~D)C-C z$2a%{zJ?^958~no01tdyr=V95+5R)dE%$-^eS@{xUJ~X^7T;?m`Nf6JOpAR9Bsoa_SL!$5Z6iT!4CE6u8bxb{nL11E z8b0Od6IveygZ(cpaI-A~GmQRQ!S|v9{|if3Z&tau3N|x0URf(^#%oSS-v{;d-i;R? z+BIx<2W0#IEaNCXdhkLMFVpn zVrweAgf zU<;Mm-i*HjoY+((#XvB7=;_AS?=NA84Me=zM`h-3`qY>F} z_4rf7KVbSt&sJJU1f$M6{$)sZx!;rz3^p$9eFmmazRzL$fC=u2Nw%k-SOn3$2j%S0=X*;c5As7 zm6V--^=cKb^l8c{5o++NI^T=L8*=X~)RzeK(Kb9mE^Nb3-sZ#z1vS5x=+)F7#>Fzl z&@5`e5M_>c*)#2ZMI?~@V?RL0k>1^tEPutParnKv*e!1Rs$;zfK9vVZm8Oj#{ze#@vlwenzLOX;Ux$W7ZdQIyiY{On8cZADZN)bzFKPpOyv;YLTF8Q~mO(V`b@ zet22ZQE^wTO9LIrptLFcMMB+DbEHTzN=D6v>B|brETuU|>!!D6rGbIq`4|inFi%1% z0Sbi3skBgG7{f*zkhVDu7-X8q-Ws#GQ;HujAA7oxFUT z=F`6a;>7hQ%D;BtP+g@}%anVrX;k&JY@3<<7EcJgF(-kT%h7FYU+LG_OT$We@pm%! z0YFW0)o3CZNx;MvuW49tJ78}2Y}lzmPTBB6keD`Nu4dy5skCvqNq^VAOUGR*NQ5u6 zZ!IcMR703uGZARi$eq1`2{d+bGOlm-4W*6L9|O4p=K|!H8R1>P{~@%(!gq6ksq=zJ ziO%B>aE>nr$it(fmeD-A?)jF-)YJp0v(?S0*W3n9h{($}hMPTzY3U6HkViBLpeN`K zFyG9I%j5_(D<;?4=2W$K{+U_M+7rE}l9 zIi3r}EQ__fwJ3&FUpltp`>#I`gz01T#E^AlxkHS9FdO)jwrSqr>8TVw?Vq{~W?^zf zBenDci3^J`Oj+;)Dq@L?X0JUHVRA!@Ba1UUb^#OQEP&|(b*x{q18;tVAESIyFVe1mst0VqYkr@H%z3(cdA*XW2W54(%h!n7a0btm zLJFTXOC5HK8rjU}mGV|R;kW7N z$zLTS6gt39h<8%%+k6z15pxM&_rPG;Y2}fkRH$4mNSImA+QFeN`1`zIs&iG2ul^wK z#c=H7|0Z=jgN0K@m(wPOs42GgkY1m-9U^N^#>EDn%Wt<-(`vOywb!1CFsTzGRd`H@ zi+jRAeHP(W$Wvnkmsi#YbVF48bfz|lO8eKhqYz&Fum8@Hv0 zwTe7@WhhRTaWs?pB7!*PX1y@AdOc7wqSf)E%S$Yq&ygst_kUZ&_6M_yi7J6#$!-A! zf|`9NVaZ)pT;9uPQXa0SEUZV&c=3+8b;MlK+DqA3;$B&{56x4p`Jp%Ofqv@1bQ5goGRiP zL<*(Sp`B@!@E&ob%&KZ7U5*6eQr2h9;k_;tZy=oJmcp3luy*AUb7@H^Rj}^29n6Tk zlqodi#3HCU7~xM#!2dCliRPY||DwE5T0!%?ij%Q<-B3n(0_FUEzp5O@&gsc^{`=Bnxf+PH8^J!V&5;U8xv|b`m63Cx? z9*3CHjf$jYpq@{N^VtLMuwVFbOb>kzB-6M4`3>y(TgS`BY#47}W17SMeo zcIQQLSoO{Vm!Bi8@2b{|0`CGhscH?^2}F`e)~s0L>Wh=u4IeOZf;FOlqOC>lp|g7& zuye5}Ik`hV7bt&HU48E-_P#Y!mQFN~Tq7wo zU^Bk~m5I`*P$Z7_GoZ0g%%YZslzWFLl-7P<@?Fh9v2SS2j=^&Lckc7MMa@=iP`fSi z*=}D|{m$r&@N4Jv4@PZs?yEi56e-_DfOXAcPrirAlx8iU6--Q_in=DQ{7ReWGBt;0 zeEx8phY!up+IfUPjSbrvszwtb=Q*Zk<#t1=C2_NMm$sRRIg^_&AZ``-Xn-}qqgsPA z4w&_19ZQ<`(NrGY!anr6!gpvLi{zXUHI8uGpH`X(uUw-af*i2gQ0RAIudg>cO_?Q< zsuj+jGNU}QmV@c62WGP7PyvNioFOy0k!Qv!KEWO&;V2mgwq-=iY*qqoG=IpuFwEj)KNJy=)kV_|cI5JP z0a*>%aVvjsTnX0P=#cRmB@OShpp)o_n=gzLtX_Y0o=h(dFirsltaPu}811|Y-3H2w zjdtycxy^h--%KC1VX6{q{vFOUpvPR~7&tEKRtG^%9ntX=?_6fus(d>4WGwN5n#>V< zM^2Au@uhZqV+L!CFi)YC{?lUAQ9p}f`Z=3kxgj_brw435-NT&-Fdc8tf;6I_TZfo} zo}<(Io2k*Wrl+C)W?sz@@VA{iP(fr3>dnRbuElVtQ^eew^WK}$^Nu+^p+ktiRDpCc zb^b$b#+H-D;+T~zIU%r3n8rt`e>i+yNPvJzki}nCLSsg5f9Z#A_eC)xHR^CcFq=@^ zF>=8r*0AZ^7nfNNwQvU!H}xJM{NRxU`|h>c3O~k_ISohq=FVYPILZp3z<=irCa-Kw zf%HCx{f;96UgjK1)^quH=zqw0%TgHhQL3-uADpNL`L+Ma*t)C=&a+MZarb2qPA`lP z#^x?dvfx;DmnPeYb8+Nw%F59=F866TWx5pjt=9g+;M1GCk;<|~U`fkaG&cFTkl}$h zB32y2atlc?WxemUP`k_OXlD{q((+4w?FsT$(NTF>tP#QB^f|_*eSc(gOTF`?T=3rx6O#V z`Gvo&31t$b|F-|9{{pnSnAnkf_J4yBw;7^xZ&grofD+;2bh@6hdAA8p;JNZWFCxO+ zNP*s8$Lc>$C7N0#H-Svi_gmM&G3K35+~iq}wX9($^d7pokjcw4*-KE-xc@#?IogVswhSz z{J&I7zl9&%*omkTim}WU8laD=>#&mX#k~BBp}5L!`S(AhT(7=3+a+{=H$KKdTVCJ| zNXlpatk)B+v1{k5;5E6EgM+`Jafiz%sj0Cx-vmWmH$#N)zo8D3?Z~aLuAU4~PH2Ul zCojjQQ^2}dR!Vg>8c5}_cHDj1LV17{_}NA54Q+SdCj9k2ikZ+dK{Nu{wY6o|VgiqG zh#S|7r_H%o&b|$GZ8P0$YCe~99n`>P1$T{kgg%>QnI?%%McW_q;PdgHZ=-67ROySM zlovAAGTuKP+(q2?D9_Q=K_82&U%NN&?-U@Eum8f#JMH^bKX2eDLvGM4SDIip4?Elg>^&Zk>uktf&;P9QWY=iMDTKA=JTy^NjR?h;)*s?ozN(}@+iK$kWBsot&2#sil_5j-9MKBh7SZOh ztIMR9jrJI!wuv7>bH_x#!yQ(YtQVa*EzLmRtsM?(ff88qC>5&W?y{e(S;j^IcSmn= zt9x=u%-Pu;uIt>zlaHG7<`2hp_>D6kAp0Xog70i|cJx*!)|Y5EX)_s`uZTg5RXJ#ykLdqvOs?*i(Wn)X?2Y+S+@-hCf zyl$%RjT@rDtOF-WtD=WgK+~C&e*y0hXKlz&psXgV(5T~Wc&apqWn0kT=8>W=397@$ z=bt^(W77C^Vd_gepxHTuc8G^K$5wqqfC<)3*xJ9eiM<44wFkx9G%e83|9ky=L0 zr{G?jETF<7cf-Z^9&jqUSn3pL(JHKsjSZe|Vei2nG%?b0Zs^efkP0$-fXdvk4|V8} z`7(s7jBY_-ePyA`ecFQW0_TU{*U@22mrbHz^|_eJPUrQ@V}UO=KL43tq6y zQA%A_hC};br^79VsFipm(=^1+iX>vzpZMR|P=vX$J+0DWDvj5CJrQUv^59xAB`Re; zwY9aL>nZzf;LBIv7@m+*AD!S@PiVF>@xESwI%M4oqjA=G2k@G-%s%kKT@cRWF-s1B zu3j%zncVb-n5l$7zsfV}<33!rr)8AdtVM02ei<2kGlR%@in_m~4$Z>Ptew-)baX1V z(hxAF*Z>d$?30(!K|xVmaa2Ja#JTz14mqC%2JqbuTVjo=BZSv$)uf_zf<45_lq=-tNc!BKp_nY0>fb8y+@T> zc}s`fGn5PMVb?eXI~cpy2~tBlqh~DzAyj?wiwqknJrbIogPCmG!W-Fy}lv}20YAB z9uh)uLq5+URArDT%zb`pvfaY#pL)GSOeUr)J2B^5CS#Gl7kleiA5)afMil6I_( zO*U}K#H5eM=DYsCiX1Vw z2^VoJ;B|2eEv4|E8JKg%W(aln<|ff}18OG2SwCrx#=Gq@Dd${4LuHho$q$Q-CYmAN zLnl<_$&D)T^E$3aMkO04VZ~V-$uip?wv6en$^0}lH1L6^K5#?rt@LNV+FdHC$;pfQ zP3uJ|C@Az4u}-AdT5LhOT^J(d^ThUJ~Xrl!yBp zr)UW{fUT2!+SZz8@?S>?b1T|5IF_Y^!A#1k5oISwtA$J(v$f@cUT zX&GmzSeZwTYy*?#SjgOi5wJM~qXm0Vu#hdK4>^UETV7-r!fiZhn4%z|4GQ9qb4rcH zX&)rG$77VhAh&OKEJnKj0cT%yH^O2k8M|~D08s5fa{z>R{cPsmaSbxzwq8#Re2=k< z^WhE`NGJdAO4x57vZ-bRzrMM78yg*!s!+3fOH4!?cr$|~cM^7r00>gKe&Q-BM*O*?X_2^;$zzV!n7nI_P_3b1^F`(FFy0(l)={AHH19v7jLL$=@(HRy zGib9qOp92X^{uhH-y2m#KU}ZkaQYD!&$U=U9TH8OwLWBHg zbizQQeJeYUuB)?HH4fWlR;#Ro-nh`k!?2n!a;6D4>)CsLJKk!QYQ@Brz#ua+M+?y5 z3H>}g0Ny>CYZS0$Z66VqJx>wmeHv9o4#v0NBE{3W{yx#r7yOEqqg8Cr_d-Ss)*=bA zKbP3T(=~z>b)`kxtxg4hciwq+c}kYz1OC=S1ufK*-h1lQI|4Z3B?&kUle65zS7e?m z3OjO2*ljggM}FGnimP&+z31>%NRXkX#SEJ36@$STL(G0G7u7AOrWei1kgJVVMt$E5 znb75}OHWqJ#p!^`{>O|HHkUo*WdQ|g4=2EP(`Vn&{_;P!n%1X*(56*+T@G}s?Ffrl zTKHNs^d;dabL6hcI#`kMnB!uTA#!Y^zJPcAV<~`TmgG;+)+$6c5FXRwpEsfRm7%0c zbPV!#0g**8SL5+X>8KeWE=$mk+DlF&{2jQfRlGY@KPN9d!J{U`cxV#$%*-8qw6x+` zg@TqtpTdpOl8kHj$nwkUmY1t8e!cDQA&#(6#}7huHV>96Zf1*o)6r_$E-+(?=wI|y zs;aNsa`Oex)^Q{3-1|vid;qQe8F+cH>^Sk7pd+&19ghE@BAnCz+w=!chx#i0gCUUO z;w*--*m1T`VRzL&r6rmb@~enyaxd@=55cZ5<7RIVL+(&F`1phlQb}K=hO_Gc_Brc@yv6Yk z5_~3ka@Kmu$jGIIZXLJPR);$A4_~B}H(Y0-`S+*qR~D2`d3UN0BqaN26AW;1qXjgB zR?_f7&VyffmLR+y_WQ;ao-rKEzW44|*zCe~T)py#JB2_OFw0yAzu$1hPfkM}!Od;C zx!xDzYqNuvLG-x{r{XFy+5#V;1;oK3O3C5nXI z!PUHA@ACGVvnX;0yatZUW!}?Zv)F7x4`@6est0(UP@}L)K)3GL+cN8URLsa(!2fvx z#;@zBKk=s7=W)quP4XgI@c+WrA+~&pV34o44%=Yw_25qV3pWyoI%NPYTKFbOU)!zT za}t2iDK%CwK)Tsflh=s-2a2B(3)a;<_Pw@>Yf(21!BP9cM+O(EYOVWG+3c)F#M#;o zQ4X(kOp|G0%_t(PF?!Au@@r9L@la19w&7K-9ZPEln;sGCFPQ;>h~mqSw1y3qIK29O z9T_9M_hDEe$t&`+>nWe!8)gN8NG48-E?{QklEXs`IjfS(-#H;r(o*@7HN;E#L(OvhTr*A9(SssMGIN7#QEE8b)#?v9jdB?~C$G z8s>FK`pfu#L}^&J-jC#IPsZdBXa4=K|D$ir!U_}Yez=KtJhv2;1xigu75jS?7OxC0NRo%sjgMqq8Ldk@ zP&ZSy5SbeuS~_9I08kQ4Q4_8Qyx!~8{tiYGJS2F2KmHE8-QkbG=R!DeB&W>tU|V?6 zVuhQz;l$BS-?wtM1-f>okXzD0J9e?))cmB$8}Nm#3u^1KX&}V07{#B0!ikU=iQHBD z@tZIuISr7i_z3+HC$B>%4OtO@sxIbklb16&Yxo$Nt^+$%dKw}%giNz|0GV66LOT^U z@Y4OMzNk!mdaBYIWlhvUSC-U;ni0*{{68B3cKQGzhD9{pPee$@c``c z-CAMFMX-dB+1eDK=tD{Q=T21tG`i?Ok$QLv9OPttd(b;3l%R24#=H3EB#C*PJh&x6 z;zh`s=BsfWmFq+EmfWUUKQ))pA?)hrN~lLR3pl%g@`Vnaf@TpHjD97~f^8LrdX(|> zbIjg>Xm133T)$v+or4EcU2b;f-Ar@nJnyDbs>8)f#ZmFk#D5xR-CBibR!fhD$oa^z zWUrNK-5&*S%i?5!3&~@7Q>4oel~Nfvnv`fW+Vb8`_S-IvUVW#g;W}9BbN;h=Hr=aIPtYM_3%6dF; zlf{>gplA}{e6$IZfB4+szQR_o1d@JRk%^#xwD9m_>-j#tJ20Fwou9 zsCAaquuczU&*b@sukvhTe9-)k}#dpEs*M+wL=?1YosW4QmEm!v)yLSu)^ zzPbdB$Lqm@JeU05Gl*K%k8!&#DoxIYg1$c`0j5zFbiflm&Ao-5SGYm;ry*#6wW~98 zDbC|&=Qv?~bAE{ZeJ@H|er}XAT}0)`8OrCWjA;cFyWOFTAD2=l0=SR`R=emGA?w2&t^4C{YFoyzB~zv;pOOmvkFI`+z~P}8&)3baL7eB zXxXsk_lsQ|D&Gk z=6y}@_T3@cexGPX7wS3u;w8ctcwnh~c-olN0fCxVwu7vC7I5HI(d@>h%Ru;lLVo(ugR3sZe^)+rZB7on zAdvhiTAj>p>{^wofjc-jDE*#)qG9{d8qhvU<15s>Pn_F+*@JJA2nX7Vr;Cc9z zmIBClVmB7}w&?|68XE8Y#0Rd1SD9N$|L zt`Cs_fKatMbZg^T-e5Xo=81o><0oVb$*_Ft3YGZ<|CjVLP94MZ+KyFvI+-~44 zUuI&W%3cFAn|GT+;zx$IO2|=fuXDl=&QAAP-FJHBtm(iXqhoZD*Ll~Sn95Lt77rgz zqH@O0U`Ft=ay6`N`?_!E)j1s1OY;Sr5_z+bC-}rMUZa;-P)h>!5i9#)$`EU zRV^N>B@Ka!ho?aM_%rW~(mwVu6t+ zEX}CwLeAPf9gq@AQdEL)_RIdyO!~KO8FX1Qv}Pkm3|u)qPvn{toJWmx07oDO;j0u# z0>IF6D_60S7?n7yg&>T~J-!BhpCwwusvG)s#oRaS_@fp12%a7^u!km=@_rdY+!1*e z8e{wEebz`w2ML_J=wHl_WuOyRBJ42-toUEKPooT%yVeuPhX2)Cq;l)hCw5pGRh%o5 z+(~a38g~$!4Kz}09obv78@3ZnSH$sMx1aK@S8kcER5xV~Ql%J?R_(xM<0!&f{t7GO z7MBcVq+oQd2_p;Fv?TK0Ys)Hwr;&|rh#UGBNtK0#O{xoh(Yb7+SkuWAsaEOvbVQE1 z(Ou=C!fZy`3Qkkr?FaSz2veU5t#rglAnFoaP6#>}GSfb(!bHO|9 zu|iV{t>R}mBKg!YYE-|Xr`J9p^3Xj9hun{USVga56WA-?+2>RNqeBH#jLh-ijvfPsfv$Vxbcy8#QU4%ayUVvaAZpvia1X_{~Tc4yMJ+9JH>hX=-Eb&O{~g>FtD%w^n9J`;sA-Rp4VMRS9nU0OSE4THc;4;f+(9V)2WM zL5fvcRx;*Q^mEHtU$WQGeR=3lpz@2HsuoHM^DY#2UDyzH{UeW*vxxPjZ+AUqu`%t` z4P^WJDU-DgjhxqGd#VaW=a+xRU$U+9A~wqIww8s7W>^?WV@WBTN3dSK{G*sk+L2SL z!1Sw+netwQis-7buVmc47o4UuaD?c_v>HnO1VQQAXNKiaebC1%wBm)fOXf+4et(tA zaC3WXUE>{7PvZT&wRboZz~k#L^y;r0g_ZSQ#>rM-bX1&G&Fei@@2kMvge@;oRfo-g zPB#2EK_4o@8A9jG_f(QraeZ*w=!1!9ubj*6e?}2|pX!Fo)}h|g1Qy3O4;52jaoded zzBtQQIG3a{e>*Z;`&a-Uw4!^e#M-h!f0gU#w~0-iKEr9^5R@c(?-3yU0+>En|{s5c46bS^A^Pb%D*Cc2*gSn#q^^m*Vs!+bq{z8+LWh=t?^{BqE|&%=wM{O>a2iFCV16@Y3mYBPt-PrQ&4%oRV z<2?ovt4G633=X&kqC4z0OxtgV&;^(3SC1b4DzpM8fmdvw8eZw`=5Fm?pd-x`sY*EO z*{7qjY94L8I$mLLB4f=t#<3MIe7<8n(0jWi6=56#0}4&->TK!vfTd|J_@!TBSNXR%o_;NZQ$8mCN0krsnbNjP5_#2u z3+J~p9uuY+bRh((rimlC;Ud=%ENkmZ&1iBl!`s!=O$CuI0p^Wk6pFpTeO{j>xL43b zsT#C3J4V2M{r^?@Q=J6VQDy;sWNDqX8yjzUp1BD74 z*jRYa9kFC_Vv3QsD?*w zQ97IV#w@Pjd7!iB>B@VLH>5ppObc*?)4SunfOAZ|*#97f;}cRe)BHgK+RM=*ls1Dk zmYO2*b^gA8uDZvMF?wJTDb|r-F{>sLOjQ8y?b3i~n)!sL$%j3)k^m{ZvH`lb9Ft3E z`6fIF+B$v)Duk6NYx6Ny!ORlfnf!Zbwjv!$86Dm%akxIX;>XzZsFq42iV>!x@msqF zMf_)_p&h@SRR>ez`}=^@bk@f!{d0L(5}jY#eIAgU?sjY@E?8Hho-0VhfdNN<1L;=8 z0M@!wNpS}K6BrN_5ASP#mOVL7^hWC?%{`}x=le3X47_2H>G~$U?)u>P3nnXRuLPs6 zFW~}ebtnSu_mA_!1TR@&yZd@NP_@hNFEK{GL{c+Xu*J|b;vy4+h?KS0g8dZ<-=vl? zyO=)U;S=?T1qRv~7etmxb0eWgVhn;K%fMyq=-|GFm$zq{=N-)@6;so?UIQnPNd|={z%Q& z=&j6{l3DBnZ{gjD1TlL@ALD!wEyK$8E6>QTFS`Z?rX9YeRgDgT#%0C{LyyHfLqvt| znsuRTMT~SDAE&5>WTYfEa@YC@zZ8QyI<&7HhL_Co;S%L zl}iT(3#2unI4F=I9sNq3+rL;^Y;&Inek|&CX9D9c+EhCV))VIG%#O-|cpMM_ zO}EGXXd#$xVP-`ClQ1Z&$!ZmjJHJCz)0jL@*zM2&iC(EwORor|kU#>UBRv>!L^!Ph# zlumAVY1nUcpDnDE4W_p-PQ2h)!ESK4PzzJNDJ>&iSER?KL*iQuk2~6Gph&#!vs07b z->*Njg8q)x6Y%0EF?nC7Nnf!O&~J^ZnM!1+nM%XfTZ1QgL|ia zd)TdYMIuukb)xw1IT5=mR3IufcoTIC*{0uzZ%$m5twyG!@0)G&wNaGN?O;YS>VM3&Lt&|w`+p+Ia4e9o8*lqmwkiN|j-4Her*5$yl(DB0y@R;{i8%w&JjTR8kZiJc~;kmTzx(VUS6C$dW2Hk6@ z-lA_kOzY0M`VqhPx2d@BFvzWgALWVBD7gkGJ7nwgI-UEzCDR07M9i3glV;g8n)rX? zI40r2T z%@Z*-)^htT?sq+uL!w~Gn~I&s?#1Ts>esI96vBlsZwUa_C^?;w zt8g1^>9ul`LWCWvAC4`R_O_Z(ztVV7y|%&)8>e@EBSsN($X7TqWYU;CUn@n3zyBaU zn7RwE_xAU@xt>l1>RdnqN=~OXW|CDAf-ljH)yD?IDk0li;H68mmGZ0mu}H2vLCV2l z>px30?Wy9_=}>pe>=-owO1G?vE3eeG{Ji2t9E!$f+e}9{^->JoVJyPe!f2Uj+LWT3 z{MP3s`THlAAq+ZfNn$YaVoGh%9!p*umL;eJaubg*yM8Nqu%1Y3X6!moxs0}11h{zy zBuu&SoBeGZ2nnqPZVcTTuC>viHlKHg0)ctUL{S!L<^i*?RREDU1253#? zQ`@xAdd^&UAyH#Xp>A>K=ME!0_~-uqMju=*;o*6wze~dtLy%}*vo#XsIShe*x3^;+ zGvh~b?|)TV4ACcw0s1>(ZRASNqKH7C`MSH|@Tlb#PA@U?l&$G46V^U+#VbjaKbi_t zYp)HEUywP<4ThikDh`>Os$>Y{w2Q&?7F9e48{mC%@0`3gRKJy?>?>W`L;pES9!m3glxz2a@f7bX#-wqtc80DjAHZwy&C!c zKb%UXM&KGn_jK9pjyVmVrB*hcPZ*Q=O5vT(ZnNfR3^_>}tv=yyge;NXCf3w9RaWx6 z_z~&%Vb1{CRd~ggjvYUuYgQOVzryjlq;x^Ivu}0LHbvwY>P}r9S=>Y8LN9~sbMC?YZD1!GqH zsPffx`OXcM4X>R<;!@Q{I#fnAMlXfhl!hMR8prc97vq2RATR~fdSE8i!5at zLAg*u`X~vAwN?}F&_`{gqdy;d!W`&8PGm7FL`%hJV4KXSpWzwnHbrwk#s~a!cHKJ? zDtjJl(9OG#&ay{P1aYYM20o4-})LM?vt4APk zI`+2HQ<`=<+XVWQhC{M=yeh(4tH4{vit%AbJ;cIpm}^g_TbX?nx_221xKTRe^!^??X?{~X*+h$E%hj>4lewXJ_O^IK1i7kkPsWA zoOKn_Acy%TiZH>&1m~b#5<<8hmZ)*#i1>=c2Rf$1BTrI2=k-2}ch(nyJAO_|dCwXf znd;`SQj94^lYP^CE;9nSEo4M)t>@_zF_Eg`^S#4GOSNv*Z~X?=S{atAOdAHUwqgse zD?VuClo2}yHqY2M#k9NcHi>EY)luPpeFtF6NBFtWMJPd_o;3&jHSM>%cG)ddMr=1a zRDC-ct?-KIo%jrmu;%Y3bl((Jw>O1DW15@D7?g$&g(=FRc13kRpo0~5O-e`#bcxeY zQV*X}Lffv(`y&VUYYZNf7Tkx0rot&mC$y0ZFI;jD8=bIF=r!JbEe<|4VmPle6?IT3 zes|b`uCnHKtO9F_O7JBecxjooTR!x4WlY8!WPve%l_ue~^Z)@R%?dj<6t-$r&5FVj z`K+4CwVxUFI~$vE85{tj;mdqlnPFiXxqvVHyzPKae@11Z>aG=#NZzoErZoMSNNCw| zoQu+9*B5u_5Vh9rFHb#z_Oma0+XrDNUmZ!kMg2!)-VwSctIb~|18ja55+B0G}G{$<4G zVynT_pIftK?;>WhOt7%adOO;x%DP7EW{ULLkeGZR=1zDKsxTR1hYJDa34wMV4B--qv#~bsX;RhTq z6Qr{v*97Vy5JBKd2bsz9m0;A*G5CE1;kn44+X)kN#0Q2Hj$f7D#Ey*W&bxP3hKyV^ zpCz7WM8v-eLqO)bM8TJTiKuNNjOYS?gEKZp5aDsEaN_ZG z%)B2jJ~l#V$-;kra^mqfF~zb&PEO;+kFoV})Oq@#bm{KXY>j)kT<%L@yT0Mt(VA)9 zGMMh}>^WOUOrEEc9egnOYSA5R4HjFC(h~=SLjDscB0A&%k3JEEL>sQAGkFYb^L|@g zMKfU&$WR$xGhFj#S5ibbDx?uFiRhIJHcXvQ5u9xfl5)=5)q-GSF9JSh869Qkgz+%Z z#6v{9UU0xDpNn>S+lrtxy6;YP%FF07g^N?(e^CMa@EgD+w(2EH;Dhz8ZRq%B1C9mH z^^Wc!3~a8-qF;0hYg%R9te|5CylkJnWK#muOp+z~L99c{Uh385a|hlQOLBFUqH#Y9 zLWJ*&_tlKQjFP!ns8mwdHBkF^@vL2j7#Nx_qR!JH;)7KJgEM?k(CC8#zxJWI+^Q8Z zO`JMQ9*JS}__Q*9n{XNT6V+wZ$#?C+HQmaC{_%ystE^k!ZP~brOX~o(wfHI$`yL;% zVLe>|;}`2w+trgbknjgoT8m-&R5E{&six53K8jtj8A`Oor`1BlmuEpL_=Vp~$|8B)qkpC}2k4L9+Oe*9AC zIO?}wTiQ76s%Qyqgx>7qj%9}7&)Uh^kkII_%et*#WLQkNXqgoBHqMz1!%Q1|p*vsqomtK>!dYRgZ z2TWeqk_zYjjVxuy0csJ2bsg#`02w#}Jn`_cXr*L{?`SKuEfjf08j&0Ku8;k1bp)wR z#0*GnGm1{$3s=10UDG$sCm5bo5!A}1B}ATuE5#XEXsSxcayV7DXMn% za~5yk$@ERv?jH`sMYQwaL&xmRrh#*XKu`f-LC7css%4P4X%KpDpDaJ<@PoFoQ39KN zfXc7Ip3`=gIc!$WwhiQg4yeY($yU1bJj&B74C;J$kGb>1T0+G-k3FsN2Xa32?G~u| z>;v3F42F!j@aebrK}QGiF-=(2a!B+)MF1o5V|}Pm;gbQ#V{eB0`{8BGP)?U*NjPz0 z`P)iXr2v_zi2%6?=M5^xm~T$l%AFsd|F|)Z0KLkUSk1S_Y`%PP)r?D?*rZH}k43dG zQymxqRlPh|T#mHztCj*L@O_%V`s;IfV1k!k4gzp^AG(#X5K78a$>;A!Zn&KZGIQ@I zw=?pJpSyQ1G+x!#@~fqB{W@|xZX5C@X-tw ze#Q?SixR5249eC~W<2eeH~@-fPTtwk<8lY>&o}2s`NE+@BD{|9lDSUVXX55{-w)|* z%GMGD#%guaAEwWzg12Z55=N3GlxV3(qSr2OFzkU0<@pG59a1f9O|+lQb?Z5Gy<`xr z6x>#mW-txMC97twgWX1GHFjd^bIk9Xr2xfRLU_w(KaAi@ul{W=%pnwD(?X9+1+2*N z>aeZ?H7!yL^K&1Oyf^!lxeZusb2>~yg>8a&z8@r=4ohkE8RU&5N+{wc z>O^X^cB5#pu6D7OBObDeVIWrYKC6&`!whL05o$3zQ;Olt4wp3P-Eru)44H({kI4_F z6YrXVI_5f9E1rc>aOZ-YP7oRC-C_DRLl5@#kG3x6vM^v)6MGJ#-563bwuztB!h_)ms4G! zd4n(VnogMKz$I>Nn`hkS&9g0z&9J|D^u7R|$h8(;6 z<);30-Doc?iF_y4D&eV5!;V3S7OWErq8`-IJ%SAQfOY%3CBK-r8rD>Bzz+Dfsn@Ke zd`xKG6ID&^!fyDQ`>NCA`Bb4jS|g!f<8Cnupn|VkDFO=MM@v~*Ssd-qL$T_D0`u*D;Eazj{Ly->P+Vt^9@@<# zD7eKtCBJbkpq$~G=O?7JDVsA+W4_)Z8AJ1_Ofh{Kg=QyWda~q?(otKqzFsKSAG!?_ z8b({1cL@2bVPRin$`JZ1LNAk3i8iQ5zT~y@YfFS;{yuM8vk5Z_MQ7&sg!sYcx6d*f zZpsUdAPrxsECUn_u!gpt{cXX3H@;OdoUgZ5Q+0QDe<4X~D0P(1IPl&>5s+;l7CDj| z1kwyefWyaxmVIVZ8XX7HUfS@9SPKZeGo1V?Zc?<_EQ(beGE^FQ%}X~3xVG zG5`lY%omWW#V`b7{JCzT>dH;N{dM0pK1)zto;=I@aC)_yNk{ z(f!d}(8}t1^-bgahHuT7DRXEA7DP9{@_F^S`j-ydFmoBFZ%?z+wI-*!(y|g7f+2x zRBfNnC7Dighb}fV#U1>9z&ZuZTU<_tr&>)XUV` zwIRBOBpDzO;K;PGAN%og6~*!OKJ!0roAO7+sVr*Q4~jD6{{f+$)o!kV4Vs*t$83h7=334ZLg0xTyjHDEar**O)Fjv1-pl2w%L z5ARyr-~^!G49Nu!e3wzbdIFN(4WymU6$ZpLb8Ihks%W~2WCk*5ntw|UK0?4+MU)R> z=zWHCZEV-~Oxo8@0v?TK*n$(4jKOI1je(gtSX-Y~8Udnr-vxe=xau&_&t5e2E773a z5t{m5M+~}atw$AG$5s8@8p)INl(5N^L6VFyVn27yLS^Hv-c@z&dAIu zixfp2$SX^1-F4P2)nsDy`VHY?wZe{T;WBJuDE{{xlw!;9S4peHse+dj4(c5RH^ zPPe=s=1H`qt0S|)9*X)mt)5!ft{%M7byOEEsF&m=;R#`oOPxPer!LV^i?IaAtH*jz z%aOk35%~!G8{71-Qeg0MTOxQ1VrL;^IMOx-HmMG(?-Fr|upUv=4r6r5WJJU+QW~%J zkF^`}`I96V@jtEn=&u^(E@cmjBNNzcTKrP5aQ#h_nSNiv^TigL#l2kq@7A_Q8!oH; zE7$1tLIxYX*ZMM6h-hJ8=~=z+XAw*~Sm@fTZ;7^RZTp^mz^QyN+V}bPVq=hf$or@X zMY=*v1LFlsFl2fTSoZ!7SoHF~wh2SkYwv}VjtW1Gi=oM1u?UW;YH!HT@Y@%~RwC)* zua-nal3WIM&oA~TO2_he$@n9rkT}u-#EMz`z$;>cw&(42U_Dzwj3wRsaH;OF|D1bt z7^8GFeFUd!1k$!Lt4-Mqxgq)p+QY5+DpagDs)_fUIvV?HW@d5kZuIQKQD-!Hb)y0D zS(M@1bpOV93dafCLfiB_e2hHZhL|pZ(2Kxn9k8B??xKIf^dhG~mvr8JewOy8{JbFw zB6!}Y76)el)MWNII7y3a6|=M{R^z84{#Q)E_DFpT?V(0h6_B3%_Y)ToonAN|d% zKy!f$qPOa&gG>1-`b_`pIxI#P8KPGu-AGd(So295a%5$hdhf!AABlztUJy8Xx8fGc zwZKLi3iii;#^bwCO>txNP|%8gQx7JaQa)GDCU8tJYpevSAedl@}VoG0TvyH z>iF8HF1oIUzo;L-{EK!`FaBDi?$>B#B+0K?0~KOI6}S$v*#;|HpFAg-ETdwPWWCvu zg&;cj!T_t<(GM$staJL_^?ezsE&+RFy+QiHNL}}8<;n+lXjz+=A9-n!=7(b{))SXf zpSa{w$BiGEhrpSBT6;0M!6ino6VSptqn_Q>MavSlPk&iuK^;7x_4pM6epZ%hf1#;R zo2KaDQ*P^)s(>w5ZZ*c5mNQ#ysOwRlnqmZNMIUbH!Fwr8RwWU%@dz z3`_bBYtZG6WDRN^+K5m5S%}5iM=72am!JYS8|6P z*Y-;6uJ)LkxHp4dlb-9Qr;c^Jn(Jy8+fZq%Ylo&P<vBxl! zM0$gS+ZnMc#dKF?P4Wzi2}GHmuJ@F{Ae+bQa{$Tzy;xL%@aTX5?hn8L``6e0|M*QB zFebU6GfHP?E27|ZwNbO9BP@{YrCE0=^P;f)Oo(fz=yZ+gW7XU2R$9A80nNs2nmcp2 z+xjiOI|M+{!M(%pHFnhyG^)c*R%<4P^|Ce$$u&aLna2ogEMYJi$V6g)C+0{Yo+YtW^yz4|19C(3;eiH6pNHC!K5O=5JM|UX z>W7>Tx9e|yn6O^tu{9YL9C|VGajbarH0Sy*+#|x`S|~0`O-oQpw5FQx_Xw+1+_KDV z5N@6Mnch1_j?v?{#>ACd4=Y%~8zKwrV{Yab2p7rk0lbf5%tO5Ad){Z~P2NPz)J4^g zHcx73meU=UNYwHjf{>y<8CK|$%!6S*5w#>JWGGYZuMrg%F>RaRqVXjMe(q@+5;GZ- z0;GQ<y zkfUIQYwIId8(?NL(Kcu1hJ+6SAw(zRFIFCftAKp0*V?(bK8)sEQ}>A-rbB|`Ia;MM2RRoTc;9o(1=vXi4VrX=KoC3hO?U@7m< z97sa%UES*C-(5LodGwX8sI;qO-68TZcvjJ1epU@1WM*g5#alb#T0I62k&PSUI5Bet zO?vP+#9Xc$-c6YpJCpP#-uWdzPn2NA1LIMe@pCcnRBln*k%aPv(r6oVk~-RIR)cLk zb-_QJM=Pe>+$up%TGPn&^}THzFM1OdeV{vYl0DgN^Cbg@&w*t;c!*1RjbiWrfk@;! zOKnHDrt-Y@(~GQ0?A-xcOdO|d&aOK7>L~^fs|>C z&MC*TM;&Cr87>nX%Djy8J7KJ$FZ;WcFq6ngLh_a{0{{)wn2}`1-+l- zzFV7vYHdz&{DIBBhwbKNGv3#~W*waRef`1fv5bsDN%OgW4EcaOoirnI{MQ5kH<3@7 ze!*VLiT@f4;N}|>kg~aRkiY;e&VR2pdBBLoJJ0g|$G!npPT~XrF~$J0pGf~_SEz#o z0S7sb&X}JfOTvut0QY-u2W?`d$e-Y#U%lQ-i_Z9w0_kG{Sq^w3yymj9k?%vQgy?Il9z%WKrG}R6IX^J z52jC)N80Sgq^)fmhqA-VYoEhwH_iWK#ujC1R63aX{QKP!qX2`<<5I!o@_kA(Qw0rD z5iwo%Ddb$NKVPFol1uR|5nT2dz3`<>BrX1Vayn{qE)IU4)!lY+5LS(wgxWzWKnFNhRy(aS#miMKMoEao{sLEM4+f zVVWdqOJ~udjNEtSkwpCcRr=!6Hs*!B8XcX_Mq-w$nj&{xZd|OYi1P2M-u#F6)I5OS zXEo@d8l_>lTa_Ibt$u_uVu1p0@m5&FrCcz}`NEgl%Ni&(fG$AoDRn^sQ?6Zo%Z~=> zpcEbaA-I$}LuUi}mpf8|3+!7`IdYNKV+&U%`%omokb1$!DKmk{ziag`g7prCwOf#o zozV9_@B*b^|E#ZX6Yry1cve9+ zLW8X3{=<&dCwBOv%AXxs5cb(&v3>FOj1ye^w9*jmVAmq)hFr7qo-+H}`UuIA^x?a` zgn!S)4m{PWM4u{AS;rfhxfuABH)d|7s8J$QQYt@=8?~dh78j27&gSQO%(Es{e2BG0 zfw5#yJkFaN`MRXzc2FianFd~|ERS*brmpr$@%8S%Ky>|@CWC=vjaCpd{02P z1Sco|KcfDfH2FWef*KeB;F!wJ2>D+}6Y!gXB;fzil{f`(f5Q9J)%pKElXzkl>i-h( dE35kvH4BwfH&t#H69M>BkWrPcmNX0ge*kh2%4Yxo literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index f6f941d9..8eb37a6c 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - Development: - Plugin and app development: - Environment Setup: DEV_ENV_SETUP.md + - Builds: BUILDS.md - Devcontainer: DEV_DEVCONTAINER.md - Custom Plugins: PLUGINS_DEV.md - Plugin Config: PLUGINS_DEV_CONFIG.md From 2e76ff1df7a2a08999f3c324f8cde150cc61c5f7 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 29 Oct 2025 13:21:12 +1100 Subject: [PATCH 05/28] DOCS: Migration and Security features navigation link --- docs/MIGRATION.md | 16 +++++++++++++--- mkdocs.yml | 3 ++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 4d7870c6..0d0da165 100755 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,6 +1,6 @@ # Migration -If upgrading from older versions of NetAlertX (or PiAlert (by jokob-sk)) the following data and setup migration steps need to be followed. +When upgrading from older versions of NetAlertX (or PiAlert (by jokob-sk)) the following data and setup migration steps need to be followed. > [!TIP] > It's always important to have a [backup strategy](./BACKUPS.md) in place. @@ -222,7 +222,10 @@ services: 3. Upgrade to `v25.10.1` by pinning the release version (See Examples below) 4. Start the container and verify everything works as expected. 5. Stop the container -6. 🔻 TBC 🔺 +6. 🔻 TBC 🔺 Perform a one-off migration to the `20211` user `docker run -it --rm --name netalertx --user "0" -v netalertx_config:/app/config -v netalertx_db:/app/db netalertx:latest` +7. 🔻 TBC 🔺 Stop the container +8. 🔻 TBC 🔺 Switch to the latest `netalertx` image +9. 🔻 TBC 🔺 Start the container and verify everything works as expected. ##### Example 1: Mapping folders @@ -245,5 +248,12 @@ services: - PORT=20211 ``` +🔻 TBC 🔺 + +```bash +docker run -it --rm --name netalertx --user "0" \ + -v netalertx_config:/app/config \ + -v netalertx_db:/app/db \ + netalertx:latest +``` -🔻 TBC 🔺 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 8eb37a6c..ea76fe53 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,7 +33,8 @@ nav: - Home Assistant: HOME_ASSISTANT.md - Emails: SMTP.md - Backups: BACKUPS.md - - Security: SECURITY.md + - Security Features: SECURITY_FEATURES.md + - Security Considerations: SECURITY.md - Advanced guides: - Remote Networks: REMOTE_NETWORKS.md - Notifications Guide: NOTIFICATIONS.md From 57f3d6f7ab6fd66e6c9fdbf127aa2363ddbef48e Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 29 Oct 2025 13:26:10 +1100 Subject: [PATCH 06/28] DOCS: Security features - fix hierarchy Signed-off-by: jokob-sk --- docs/SECURITY_FEATURES.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/SECURITY_FEATURES.md b/docs/SECURITY_FEATURES.md index 7fd5ae57..9832639e 100644 --- a/docs/SECURITY_FEATURES.md +++ b/docs/SECURITY_FEATURES.md @@ -6,7 +6,7 @@ NetAlertX is engineered from the ground up to prevent this. It's not just an app Here’s a breakdown of the defensive layers you get, right out of the box using the default configuration. -### Feature 1: The "Digital Concrete" Filesystem +## Feature 1: The "Digital Concrete" Filesystem **Methodology:** The core application and its system files are treated as immutable. Once built, the app's code is "set in concrete," preventing attackers from modifying it or planting malware. @@ -18,7 +18,7 @@ Here’s a breakdown of the defensive layers you get, right out of the box using **What's this mean to you:** Even if an attacker gets in, they **cannot modify the application code or plant malware.** It's like the app is set in digital concrete. -### Feature 2: Surgical, "Keycard-Only" Access +## Feature 2: Surgical, "Keycard-Only" Access **Methodology:** The principle of least privilege is strictly enforced. Every process gets only the absolute minimum set of permissions needed for its specific job. @@ -30,7 +30,7 @@ Here’s a breakdown of the defensive layers you get, right out of the box using **What's this mean to you:** A security breach is **firewalled.** An attacker who gets into the web UI **does not have the "keycard"** to start scanning your network or take over the system. The breach is contained. -### Feature 3: Attack Surface "Amputation" +## Feature 3: Attack Surface "Amputation" **Methodology:** The potential attack surface is aggressively minimized by removing every non-essential tool an attacker would want to use. @@ -44,7 +44,7 @@ Here’s a breakdown of the defensive layers you get, right out of the box using **What's this mean to you:** An attacker who breaks in finds themselves in an **empty room with no tools.** They have no `sudo` to get more power, no package manager to download weapons, and no compilers to build new ones. -### Feature 4: "Self-Cleaning" Writable Areas +## Feature 4: "Self-Cleaning" Writable Areas **Methodology:** All writable locations are treated as untrusted, temporary, and non-executable by default. @@ -56,7 +56,7 @@ Here’s a breakdown of the defensive layers you get, right out of the box using **What's this mean to you:** Any malicious file an attacker *does* manage to drop is **written in invisible, non-permanent ink.** The file is written to RAM, not disk, so it **vaporizes the instant you restart** the container. Even worse for them, the `noexec` flag means they **can't even run the file** in the first place. -### Feature 5: Built-in Resource Guardrails +## Feature 5: Built-in Resource Guardrails **Methodology:** The container is constrained by resource limits to function as a "good citizen" on the host system. This prevents a compromised or runaway process from consuming excessive resources, a common vector for Denial of Service (DoS) attacks. @@ -66,7 +66,7 @@ Here’s a breakdown of the defensive layers you get, right out of the box using **What's this mean to you:** NetAlertX is a "good neighbor" and **can't be used to crash your host machine.** Even if a process is compromised, it's in a digital straitjacket and **cannot** pull a "denial of service" attack by hogging all your CPU or memory. -### Feature 6: The "Pre-Flight" Self-Check +## Feature 6: The "Pre-Flight" Self-Check **Methodology:** Before any services start, NetAlertX runs a comprehensive "pre-flight" check to ensure its own security and configuration are sound. It's like a built-in auditor that verifies its own defenses. @@ -78,7 +78,7 @@ Here’s a breakdown of the defensive layers you get, right out of the box using **What's this mean to you:** The system is **self-aware and checks its own work.** You get instant feedback if a setting is wrong, and you get peace of mind on every single boot knowing all these security layers are **active and verified,** all in about one second. -### Conclusion: Security by Default +## Conclusion: Security by Default No single security control is a silver bullet. The robust security posture of NetAlertX is achieved through **defense in depth**, layering these methodologies. From 61de63771ba4a3560bac9cce4d6740e789a6a4eb Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 29 Oct 2025 15:51:31 +1100 Subject: [PATCH 07/28] DOCS: Docker guides Signed-off-by: jokob-sk --- docs/BACKUPS.md | 210 +++++++++++++++++++++++++------------ docs/DOCKER_MAINTENANCE.md | 200 +++++++++++++++++++++++++++++++++++ docs/FILE_PERMISSIONS.md | 23 ++++ docs/LOGGING.md | 59 ++++++++++- mkdocs.yml | 1 + 5 files changed, 419 insertions(+), 74 deletions(-) create mode 100644 docs/DOCKER_MAINTENANCE.md diff --git a/docs/BACKUPS.md b/docs/BACKUPS.md index 217eab1e..965f7f15 100755 --- a/docs/BACKUPS.md +++ b/docs/BACKUPS.md @@ -1,90 +1,162 @@ -# Backing things up +# Backing Things Up > [!NOTE] -> To backup 99% of your configuration backup at least the `/app/config` folder. Please read the whole page (or at least "Scenario 2: Corrupted database") for details. -> Note that database definitions might change over time. The safest way is to restore your older backups into the **same version** of the app they were taken from and then gradually upgarde between releases to the latest version. +> To back up 99% of your configuration, back up at least the `/app/config` folder. +> Database definitions can change between releases, so the safest method is to restore backups using the **same app version** they were taken from, then upgrade incrementally. -There are 4 artifacts that can be used to backup the application: +--- -| File | Description | Limitations | -|-----------------------|-------------------------------|-------------------------------| -| `/db/app.db` | Database file(s) | The database file might be in an uncommitted state or corrupted | -| `/config/app.conf` | Configuration file | Can be overridden with the [`APP_CONF_OVERRIDE` env variable](https://github.com/jokob-sk/NetAlertX/tree/main/dockerfiles#docker-environment-variables). | -| `/config/devices.csv` | CSV file containing device information | Doesn't contain historical data | -| `/config/workflows.json` | A JSON file containing your workflows | N/A | +## What to Back Up +There are four key artifacts you can use to back up your NetAlertX configuration: -## Backup strategies +| File | Description | Limitations | +| ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/db/app.db` | The application database | Might be in an uncommitted state or corrupted | +| `/config/app.conf` | Configuration file | Can be overridden using the [`APP_CONF_OVERRIDE`](https://github.com/jokob-sk/NetAlertX/tree/main/dockerfiles#docker-environment-variables) variable | +| `/config/devices.csv` | CSV file containing device data | Does not include historical data | +| `/config/workflows.json` | JSON file containing your workflows | N/A | -The safest approach to backups is to backup everything, by taking regular file system backups of the `/db` and `/config` folders (I use [Kopia](https://github.com/kopia/kopia)). +--- -Arguably, the most time is spent setting up the device list, so if only one file is kept I'd recommend to have a latest backup of the `devices_.csv` or `devices.csv` file, followed by the `app.conf` and `workflows.json` files. You can also download `app.conf` and `devices.csv` file in the Maintenance section: +## Where the Data Lives -![Backup and Restore Section in Maintenance](./img/BACKUPS/Maintenance_Backup_Restore.png) - -### Scenario 1: Full backup - -End-result: Full restore - -#### 💾 Source artifacts: - -- `/app/db/app.db` (uncorrupted) -- `/app/config/app.conf` -- `/app/config/workflows.json` - -#### 📥 Recovery: - -To restore the application map the above files as described in the [Setup documentation](https://github.com/jokob-sk/NetAlertX/blob/main/dockerfiles/README.md#docker-paths). - - -### Scenario 2: Corrupted database - -End-result: Partial restore (historical data and some plugin data will be missing) - -#### 💾 Source artifacts: - -- `/app/config/app.conf` -- `/app/config/devices_.csv` or `/app/config/devices.csv` -- `/app/config/workflows.json` - -#### 📥 Recovery: - -Even with a corrupted database you can recover what I would argue is 99% of the configuration. - -- upload the `app.conf` and `workflows.json` files into the mounted `/app/config/` folder as described in the [Setup documentation](https://github.com/jokob-sk/NetAlertX/blob/main/dockerfiles/README.md#docker-paths). -- rename the `devices_.csv` to `devices.csv` and place it in the `/app/config` folder -- Restore the `devices.csv` backup via the [Maintenance section](./DEVICES_BULK_EDITING.md) - -## Data and backup storage - -To decide on a backup strategy, check where the data is stored: +Understanding where your data is stored helps you plan your backup strategy. ### Core Configuration -The core application configuration is in the `app.conf` file (See [Settings System](./SETTINGS_SYSTEM.md) for details), such as: +Stored in `/app/config/app.conf`. +This includes settings for: -- Notification settings -- Scanner settings -- Scheduled maintenance settings -- UI configuration +* Notifications +* Scanning +* Scheduled maintenance +* UI preferences -### Core Device Data +(See [Settings System](./SETTINGS_SYSTEM.md) for details.) -The core device data is backed up to the `devices_.csv` or `devices.csv` file via the [CSV Backup `CSVBCKP` Plugin](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/csv_backup). This file contains data, such as: +### Device Data -- Device names -- Device icons -- Device network configuration -- Device categorization -- Device custom properties data +Stored in `/app/config/devices_.csv` or `/app/config/devices.csv`, created by the [CSV Backup `CSVBCKP` Plugin](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/csv_backup). +Contains: -### Historical data +* Device names, icons, and categories +* Network configuration +* Custom properties -Historical data is stored in the `app.db` database (See [Database overview](./DATABASE.md) for details). This data includes: +### Historical Data -- Plugin objects -- Plugin historical entries -- History of Events, Notifications, Workflow Events -- Presence history +Stored in `/app/db/app.db` (see [Database Overview](./DATABASE.md)). +Contains: +* Plugin data and historical entries +* Event and notification history +* Device presence history +--- + +## Backup Strategies + +The safest approach is to back up **both** the `/db` and `/config` folders regularly. Tools like [Kopia](https://github.com/kopia/kopia) make this simple and efficient. + +If you can only keep a few files, prioritize: + +1. The latest `devices_.csv` or `devices.csv` +2. `app.conf` +3. `workflows.json` + +You can also download the `app.conf` and `devices.csv` files from the **Maintenance** section: + +![Backup and Restore Section in Maintenance](./img/BACKUPS/Maintenance_Backup_Restore.png) + +--- + +## Scenario 1: Full Backup and Restore + +**Goal:** Full recovery of your configuration and data. + +### 💾 What to Back Up + +* `/app/db/app.db` (uncorrupted) +* `/app/config/app.conf` +* `/app/config/workflows.json` + +### 📥 How to Restore + +Map these files into your container as described in the [Setup documentation](https://github.com/jokob-sk/NetAlertX/blob/main/dockerfiles/README.md#docker-paths). + +--- + +## Scenario 2: Corrupted Database + +**Goal:** Recover configuration and device data when the database is lost or corrupted. + +### 💾 What to Back Up + +* `/app/config/app.conf` +* `/app/config/workflows.json` +* `/app/config/devices_.csv` (rename to `devices.csv` during restore) + +### 📥 How to Restore + +1. Copy `app.conf` and `workflows.json` into `/app/config/` +2. Rename and place `devices_.csv` → `/app/config/devices.csv` +3. Restore via the **Maintenance** section under *Devices → Bulk Editing* + +This recovers nearly all configuration, workflows, and device metadata. + +--- + +## Docker-Based Backup and Restore + +For users running NetAlertX via Docker, you can back up or restore directly from your host system — a convenient and scriptable option. + +### Full Backup (File-Level) + +1. **Stop the container:** + + ```bash + docker stop netalertx + ``` + +2. **Create a compressed archive** of your configuration and database volumes: + + ```bash + docker run --rm -v netalertx_config:/config -v netalertx_db:/db alpine tar -cz /config /db > netalertx-backup.tar.gz + ``` + +3. **Restart the container:** + + ```bash + docker start netalertx + ``` + +### Restore from Backup + +1. **Stop the container:** + + ```bash + docker stop netalertx + ``` + +2. **Restore from your backup file:** + + ```bash + docker run --rm -i -v netalertx_config:/config -v netalertx_db:/db alpine tar -C / -xz < netalertx-backup.tar.gz + ``` + +3. **Restart the container:** + + ```bash + docker start netalertx + ``` + +> This approach uses a temporary, minimal `alpine` container to access Docker-managed volumes. The `tar` command creates or extracts an archive directly from your host’s filesystem, making it fast, clean, and reliable for both automation and manual recovery. + +--- + +## Summary + +* Back up `/app/config` for configuration and devices; `/app/db` for history +* Keep regular backups, especially before upgrades +* For Docker setups, use the lightweight `alpine`-based backup method for consistency and portability diff --git a/docs/DOCKER_MAINTENANCE.md b/docs/DOCKER_MAINTENANCE.md new file mode 100644 index 00000000..f696c0eb --- /dev/null +++ b/docs/DOCKER_MAINTENANCE.md @@ -0,0 +1,200 @@ +# The NetAlertX Container Operator's Guide + +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 + + +> [!NOTE] +> +> Other relevant resources +> - [Fixing Permission Issues](./FILE_PERMISSIONS.md) +> - [Handling Backups](./BACKUPS.md) +> - [Accessing Application Logs](./LOGGING.md) + +--- + +## Task: Using a Local Folder for Configuration + +### Problem + +You want to edit your `app.conf` and other configuration files directly from your host machine, instead of using a Docker-managed volume. + +### Solution + +1. Stop the container: + + ```bash + docker-compose down + ``` +2. (Optional but Recommended) Back up your data using the method in Part 1. +3. Create a local folder on your host machine (e.g., `/data/netalertx_config`). +4. Edit `docker-compose.yml`: + + * **Comment out** the `netalertx_config` volume entry. + * **Uncomment** and **set the path** for the "Example custom local folder" bind mount entry. + + ```yaml + ... + volumes: + # - type: volume + # source: netalertx_config + # target: /app/config + # read_only: false + ... + # Example custom local folder called /data/netalertx_config + - type: bind + source: /data/netalertx_config + target: /app/config + read_only: false + ... + ``` +5. (Optional) Restore your backup. +6. Restart the container: + + ```bash + docker-compose up -d + ``` + +### About This Method + +This replaces the Docker-managed volume with a "bind mount." This is a direct mapping between a folder on your host computer (`/data/netalertx_config`) and a folder inside the container (`/app/config`), allowing you to edit the files directly. + +--- + +## Task: Migrating from a Local Folder to a Docker Volume + +### Problem + +You are currently using a local folder (bind mount) for your configuration (e.g., `/data/netalertx_config`) and want to switch to the recommended Docker-managed volume (`netalertx_config`). + +### Solution + +1. Stop the container: + + ```bash + docker-compose down + ``` +2. Edit `docker-compose.yml`: + + * **Comment out** the bind mount entry for your local folder. + * **Uncomment** the `netalertx_config` volume entry. + + ```yaml + ... + volumes: + - type: volume + source: netalertx_config + target: /app/config + read_only: false + ... + # Example custom local folder called /data/netalertx_config + # - type: bind + # source: /data/netalertx_config + # target: /app/config + # read_only: false + ... + ``` +3. (Optional) Initialize the volume: + + ```bash + docker-compose up -d && docker-compose down + ``` +4. Run the migration command (**replace `/data/netalertx_config` with your actual path**): + + ```bash + docker run --rm -v netalertx_config:/config -v /data/netalertx_config:/local-config alpine \ + sh -c "tar -C /local-config -c . | tar -C /config -x" + ``` +5. Restart the container: + + ```bash + docker-compose up -d + ``` + +### About This Method + +This uses a temporary `alpine` container that mounts *both* your source folder (`/local-config`) and destination volume (`/config`). The `tar ... | tar ...` command safely copies all files, including hidden ones, preserving structure. + +--- + +## Task: Applying a Custom Nginx Configuration + +### Problem + +You need to override the default Nginx configuration to add features like LDAP, SSO, or custom SSL settings. + +### Solution + +1. Stop the container: + + ```bash + docker-compose down + ``` +2. Create your custom config file on your host (e.g., `/data/my-netalertx.conf`). +3. Edit `docker-compose.yml`: + + ```yaml + ... + # Use a custom Enterprise-configured nginx config for ldap or other settings + - /data/my-netalertx.conf:/services/config/nginx/conf.active/netalertx.conf:ro + ... + ``` +4. Restart the container: + + ```bash + docker-compose up -d + ``` + +### About This Method + +Docker’s bind mount overlays your host file (`my-netalertx.conf`) on top of the default file inside the container. The container remains read-only, but Nginx reads your file as if it were the default. + +--- + +## Task: Mounting Additional Files for Plugins + +### Problem + +A plugin (like `DHCPLSS`) needs to read a file from your host machine (e.g., `/var/lib/dhcp/dhcpd.leases`). + +### Solution + +1. Stop the container: + + ```bash + docker-compose down + ``` +2. Edit `docker-compose.yml` and add a new line under the `volumes:` section: + + ```yaml + ... + volumes: + ... + # Mount for DHCPLSS plugin + - /var/lib/dhcp/dhcpd.leases:/mnt/dhcpd.leases:ro + ... + ``` +3. Restart the container: + + ```bash + docker-compose up -d + ``` +4. In the NetAlertX web UI, configure the plugin to read from: + + ``` + /mnt/dhcpd.leases + ``` + +### About This Method + +This maps your host file to a new, read-only (`:ro`) location inside the container. The plugin can then safely read this file without exposing anything else on your host filesystem. + + diff --git a/docs/FILE_PERMISSIONS.md b/docs/FILE_PERMISSIONS.md index e9b757a8..42ccd603 100755 --- a/docs/FILE_PERMISSIONS.md +++ b/docs/FILE_PERMISSIONS.md @@ -21,3 +21,26 @@ Option to set specific user UID and GID can be useful for host system users need |----------------|--------|---------|-----------|----------|-------------|---------------------------------------------------------------------| | `/app/config` | nginx | PUID (default 102) | www-data | PGID (default 82) | rwxr-xr-x | Ensure `nginx` can read/write; other users can read if in `www-data` | | `/app/db` | nginx | PUID (default 102) | www-data | PGID (default 82) | rwxr-xr-x | Same as above | + + +### Fixing Permission Problems + +The container fails to start with "Permission Denied" errors. This typically happens when your data volumes (`netalertx_config`, `netalertx_db`) are "owned" by the `root` user (UID 0) from a previous install, but the secure container must run as the `netalertx` user (UID 20211). + +### Solution + +1. Run the container **once** as the `root` user (`--user "0"`) to trigger the built-in fix-it mode: + + ```bash + docker run -it --rm --name netalertx-fix --user "0" \ + -v netalertx_config:/app/config \ + -v netalertx_db:/app/db \ + netalertx:latest + ``` +2. Wait for the logs to show a **magenta warning** and confirm permissions are being fixed. The container will then hang (this is intentional). +3. Press **Ctrl+C** to stop the fix-it container. +4. Start your container normally (e.g., with `docker-compose up -d` or your standard `docker run` command). + +### About This Method + +The container’s startup script detects it is running as `root` (UID 0). It automatically runs the `chown` command to fix the permissions on your volume files, setting them to the correct `20211` user. It then hangs (`sleep infinity`) to prevent you from *ever* running the application as `root`, forcing you to restart securely. \ No newline at end of file diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 8be0cc82..8fcc1449 100755 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -1,11 +1,9 @@ # Logging -NetAlertX comes with several logs that help to identify application issues. - -For plugin-specific log debugging, please read the [Debug Plugins](./DEBUG_PLUGINS.md) guide. - -When debugging any issue, increase the `LOG_LEVEL` Setting as per the [Debug tips](./DEBUG_TIPS.md) documentation. +NetAlertX comes with several logs that help to identify application issues. These include ngnix logs, app, or plugin logs. For plugin-specific log debugging, please read the [Debug Plugins](./DEBUG_PLUGINS.md) guide. +> [!NOTE] +> When debugging any issue, increase the `LOG_LEVEL` Setting as per the [Debug tips](./DEBUG_TIPS.md) documentation. ## Main logs @@ -24,3 +22,54 @@ If a Plugin supplies data to the main app it's done either vie a SQL query or vi The data is in most of the cases then displayed in the application under _Integrations -> Plugins_ (or _Device -> Plugins_ if the plugin is supplying device-specific data). ![Plugin objects](./img/LOGGING/logging_integrations_plugins.png) + +## Viewing Logs on the File System + +You cannot find any log files on the filesystem. The container is `read-only` and writes logs to a temporary in-memory filesystem (`tmpfs`) for security and performance. The application follows container best-practices by writing all logs to the standard output (`stdout`) and standard error (`stderr`) streams. Docker's logging driver (set in `docker-compose.yml`) captures this stream automatically, allowing you to access it with the `docker logs ` command. + +* **To see all logs since the last restart:** + + ```bash + docker logs netalertx + ``` +* **To watch the logs live (live feed):** + + ```bash + docker logs -f netalertx + ``` +## Enabling Persistent File-Based Logs + +The default logs are erased every time the container restarts because they are stored in temporary in-memory storage (`tmpfs`). If you need to keep a persistent, file-based log history, follow the steps below. + +> [!NOTE] +> This might lead to performance degradation so this approach is only suggested when activelly debuging issues. See the [Performance optimization](./PERFORMANCE.md) documentation for details. + +1. Stop the container: + + ```bash + docker-compose down + ``` + +2. Edit your `docker-compose.yml` file: + + * **Comment out** the `/app/log` line under the `tmpfs:` section. + * **Uncomment** the "Retain logs" line under the `volumes:` section and set your desired host path. + + ```yaml + ... + tmpfs: + # - "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + ... + volumes: + ... + # Retain logs - comment out tmpfs /app/log if you want to retain logs between container restarts + - /home/adam/netalertx_logs:/app/log + ... + ``` +3. Restart the container: + + ```bash + docker-compose up -d + ``` + +This change stops Docker from mounting a temporary in-memory volume at `/app/log`. Instead, it "bind mounts" a persistent folder from your host computer (e.g., `/data/netalertx_logs`) to that *exact same location* inside the container. diff --git a/mkdocs.yml b/mkdocs.yml index ea76fe53..e9936c6d 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ nav: - Docker Compose: DOCKER_COMPOSE.md - Docker File Permissions: FILE_PERMISSIONS.md - Docker Updates: UPDATES.md + - Docker Maintenance: DOCKER_MAINTENANCE.md - Other: - Synology Guide: SYNOLOGY_GUIDE.md - Portainer Stacks: DOCKER_PORTAINER.md From 2148a7ffc5489646a55d049dd195c666868caacf Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 29 Oct 2025 20:33:32 +1100 Subject: [PATCH 08/28] DOCS: Docker guides Signed-off-by: jokob-sk --- docs/BACKUPS.md | 4 ++-- docs/LOGGING.md | 6 +++--- docs/SECURITY_FEATURES.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/BACKUPS.md b/docs/BACKUPS.md index 965f7f15..9e2fd679 100755 --- a/docs/BACKUPS.md +++ b/docs/BACKUPS.md @@ -122,7 +122,7 @@ For users running NetAlertX via Docker, you can back up or restore directly from 2. **Create a compressed archive** of your configuration and database volumes: ```bash - docker run --rm -v netalertx_config:/config -v netalertx_db:/db alpine tar -cz /config /db > netalertx-backup.tar.gz + docker run --rm -v local_path/config:/config -v local_path/db:/db alpine tar -cz /config /db > netalertx-backup.tar.gz ``` 3. **Restart the container:** @@ -142,7 +142,7 @@ For users running NetAlertX via Docker, you can back up or restore directly from 2. **Restore from your backup file:** ```bash - docker run --rm -i -v netalertx_config:/config -v netalertx_db:/db alpine tar -C / -xz < netalertx-backup.tar.gz + docker run --rm -i -v local_path/config:/config -v local_path/db:/db alpine tar -C / -xz < netalertx-backup.tar.gz ``` 3. **Restart the container:** diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 8fcc1449..b26eac38 100755 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -1,6 +1,6 @@ # Logging -NetAlertX comes with several logs that help to identify application issues. These include ngnix logs, app, or plugin logs. For plugin-specific log debugging, please read the [Debug Plugins](./DEBUG_PLUGINS.md) guide. +NetAlertX comes with several logs that help to identify application issues. These include nginx logs, app, or plugin logs. For plugin-specific log debugging, please read the [Debug Plugins](./DEBUG_PLUGINS.md) guide. > [!NOTE] > When debugging any issue, increase the `LOG_LEVEL` Setting as per the [Debug tips](./DEBUG_TIPS.md) documentation. @@ -42,7 +42,7 @@ You cannot find any log files on the filesystem. The container is `read-only` an The default logs are erased every time the container restarts because they are stored in temporary in-memory storage (`tmpfs`). If you need to keep a persistent, file-based log history, follow the steps below. > [!NOTE] -> This might lead to performance degradation so this approach is only suggested when activelly debuging issues. See the [Performance optimization](./PERFORMANCE.md) documentation for details. +> This might lead to performance degradation so this approach is only suggested when actively debugging issues. See the [Performance optimization](./PERFORMANCE.md) documentation for details. 1. Stop the container: @@ -72,4 +72,4 @@ The default logs are erased every time the container restarts because they are s docker-compose up -d ``` -This change stops Docker from mounting a temporary in-memory volume at `/app/log`. Instead, it "bind mounts" a persistent folder from your host computer (e.g., `/data/netalertx_logs`) to that *exact same location* inside the container. +This change stops Docker from mounting a temporary in-memory volume at `/app/log`. Instead, it "bind mounts" a persistent folder from your host computer (e.g., `/data/netalertx_logs`) to that *same location* inside the container. diff --git a/docs/SECURITY_FEATURES.md b/docs/SECURITY_FEATURES.md index 9832639e..c505a990 100644 --- a/docs/SECURITY_FEATURES.md +++ b/docs/SECURITY_FEATURES.md @@ -68,7 +68,7 @@ Here’s a breakdown of the defensive layers you get, right out of the box using ## Feature 6: The "Pre-Flight" Self-Check -**Methodology:** Before any services start, NetAlertX runs a comprehensive "pre-flight" check to ensure its own security and configuration are sound. It's like a built-in auditor that verifies its own defenses. +**Methodology:** Before any services start, NetAlertX runs a comprehensive "pre-flight" check to ensure its own security and configuration are sound. It's like a built-in auditor who verifies its own defenses. * **Active Self-Diagnosis:** On every single boot, NetAlertX runs a series of startup pre-checks—and it's fast. The entire self-check process typically completes in less than a second, letting you get to the web UI in about three seconds from startup. From af80cff8e030d9da2f787a02e683848cbbfb1fc3 Mon Sep 17 00:00:00 2001 From: Tweebloesem <139498987+Tweebloesem@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:18:42 +0100 Subject: [PATCH 09/28] Fix typo in PiHole integration guide --- docs/PIHOLE_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PIHOLE_GUIDE.md b/docs/PIHOLE_GUIDE.md index 656dad57..62164667 100755 --- a/docs/PIHOLE_GUIDE.md +++ b/docs/PIHOLE_GUIDE.md @@ -1,6 +1,6 @@ # Integration with PiHole -NetAlertX comes with 2 plugins suitable for integarting with your existing PiHole instace. 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 2 plugins suitable for integrating with your existing PiHole instace. 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). ## Approach 1: `DHCPLSS` Plugin - Import devices from the PiHole DHCP leases file From 55171e06b611e79a3bcae24a7e9504b3c1a77623 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Wed, 29 Oct 2025 23:29:32 +0000 Subject: [PATCH 10/28] update compose --- docs/DOCKER_COMPOSE.md | 359 +++++++++++++++++++++++------------------ 1 file changed, 201 insertions(+), 158 deletions(-) diff --git a/docs/DOCKER_COMPOSE.md b/docs/DOCKER_COMPOSE.md index a59161ef..77e98a28 100755 --- a/docs/DOCKER_COMPOSE.md +++ b/docs/DOCKER_COMPOSE.md @@ -1,203 +1,246 @@ -# `docker-compose.yaml` Examples +# NetAlertX and Docker Compose + +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"`. This also means that not all functionality is supported on a Windows host as Docker for Windows doesn't support this networking option. +> 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.s operating system. -### Example 1 +## Baseline Docker Compose -```yaml +There is one baseline for NetAlertX. That's the default security-enabled official distribution. + +``` services: netalertx: - container_name: netalertx - # use the below line if you want to test the latest dev image - # image: "ghcr.io/jokob-sk/netalertx-dev:latest" - image: "ghcr.io/jokob-sk/netalertx:latest" - network_mode: "host" - restart: unless-stopped + #use an environmental variable to set host networking mode if needed + container_name: netalertx # The name when you docker contiainer ls + image: ghcr.io/jokob-sk/netalertx-dev:latest + network_mode: ${NETALERTX_NETWORK_MODE:-host} # Use host networking for ARP scanning and other services + + read_only: true # Make the container filesystem read-only + cap_drop: # Drop all capabilities for enhanced security + - ALL + cap_add: # Add only the necessary capabilities + - NET_ADMIN # Required for ARP scanning + - NET_RAW # Required for raw socket operations + - NET_BIND_SERVICE # Required to bind to privileged ports (nbtscan) + volumes: - - local_path/config:/app/config - - local_path/db:/app/db - # (optional) useful for debugging if you have issues setting up the container - - local_path/logs:/app/log - # (API: OPTION 1) use for performance - - type: tmpfs - target: /app/api - # (API: OPTION 2) use when debugging issues - # - local_path/api:/app/api + - type: volume # Persistent Docker-managed Named Volume for storage of config files + source: netalertx_config # the default name of the volume is netalertx_config + target: /app/config # inside the container mounted to /app/config + read_only: false # writable volume + + # Example custom local folder called /home/user/netalertx_config + # - type: bind + # source: /home/user/netalertx_config + # target: /app/config + # read_only: false + # ... or use the alternative format + # - /home/user/netalertx_config:/app/config:rw + + - type: volume # NetAlertX Database partiton + source: netalertx_db + target: /app/db + read_only: false + + - type: volume # Future proof mount. During the migration to a + source: netalertx_data # future version, app and db will be migrated to + target: /data # the /data partition. This will reduce the + read_only: false # overhead and pain in the upcoming migration. + + - type: bind # Bind mount for timezone consistency + source: /etc/localtime + target: /etc/localtime + read_only: true + + # Mount your DHCP server file into NetAlertX for a plugin to access + # - path/on/host/to/dhcp.file:/resources/dhcp.file + + # Retain logs - comment out tmpfs /app/log if you want to retain logs between container restarts + # - /path/on/host/log:/app/log + + # Tempfs mounts for writable directories in a read-only container and improve system performance + # All mounts have noexec,nosuid,nodev for security purposes no devices, no suid/sgid and no execution of binaries + # async where possible for performance, sync where required for correctness + # uid=20211 and gid=20211 is the netalertx user inside the container + # mode=1700 gives rwx------ permissions to the netalertx user only + tmpfs: + # Speed up logging. This can be commented out to retain logs between container restarts + - "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # Speed up API access as frontend/backend API is very chatty + - "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,sync,noatime,nodiratime" + # Required for customization of the nginx listen addr/port without rebuilding the container + - "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # /services/config/nginx/conf.d is required for nginx and php to start + - "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # /tmp is required by php for session save this should be reworked to /services/run/tmp + - "/tmp:uid=2Key-Value Pairs: 20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" environment: - - TZ=Europe/Berlin - - PORT=20211 + LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces + PORT: ${PORT:-20211} # Application port + APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212} # GraphQL API port (passed as APP_CONF_OVERRIDE) + NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} # 0=kill all services and restart if any dies. 1 keeps running dead services. + + # Resource limits to prevent resource exhaustion + mem_limit: 2048m # Maximum memory usage + mem_reservation: 1024m # Soft memory limit + cpu_shares: 512 # Relative CPU weight for CPU contention scenarios + pids_limit: 512 # Limit the number of processes/threads to prevent fork bombs + logging: + driver: "json-file" # Use JSON file logging driver + options: + max-size: "10m" # Rotate log files after they reach 10MB + max-file: "3" # Keep a maximum of 3 log files + + # Always restart the container unless explicitly stopped + restart: unless-stopped + +volumes: # Persistent volumes for configuration and database storage + netalertx_config: # Configuration files + netalertx_db: # Database files + netalertx_data: # For future config/db upgrade ``` -To run the container execute: `sudo docker-compose up -d` +Run or re-run it: -### Example 2 - -Example by [SeimuS](https://github.com/SeimusS). - -```yaml -services: - netalertx: - container_name: NetAlertX - hostname: NetAlertX - privileged: true - # use the below line if you want to test the latest dev image - # image: "ghcr.io/jokob-sk/netalertx-dev:latest" - image: ghcr.io/jokob-sk/netalertx:latest - environment: - - TZ=Europe/Bratislava - restart: always - volumes: - - ./netalertx/db:/app/db - - ./netalertx/config:/app/config - network_mode: host +``` +docker compose up --force-recreate ``` -To run the container execute: `sudo docker-compose up -d` +### Customize with Environmental Variables -### Example 3 +You can override the default settings by passing environmental variables to the `docker compose up` command. -`docker-compose.yml` +**Example using a single variable:** -```yaml +This command runs NetAlertX on port 8080 instead of the default 20211. + +``` +PORT=8080 docker compose up +``` + +**Example using all available variables:** + +This command demonstrates overriding all primary environmental variables: running with host networking, on port 20211, GraphQL on 20212, and listening on all IPs. + +``` +NETALERTX_NETWORK_MODE=host \ +LISTEN_ADDR=0.0.0.0 \ +PORT=20211 \ +GRAPHQL_PORT=20212 \ +NETALERTX_DEBUG=0 \ +docker compose up +``` + +## `docker-compose.yaml` Modifications + +### Modification 1: Use a Local Folder (Bind Mount) + +By default, the baseline compose file uses "named volumes" (`netalertx_config`, `netalertx_db`). **This is the preferred method** because NetAlertX is designed to manage all configuration and database settings directly from its web UI. Named volumes let Docker handle this data cleanly without you needing to manage local file permissions or paths. + +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. + +**How to make the change:** + +1. Choose a location on your computer. For example, `/home/adam/netalertx-files`. + +2. Create the subfolders: `mkdir -p /home/adam/netalertx-files/config` and `mkdir -p /home/adam/netalertx-files/db`. + +3. Edit your `docker-compose.yml` and find the `volumes:` section (the one *inside* the `netalertx:` service). + +4. Comment out (add a `#` in front) or delete the `type: volume` blocks for `netalertx_config` and `netalertx_db`. + +5. Add new lines pointing to your local folders. + +**Before (Using Named Volumes - Preferred):** + +``` +... + volumes: + - netalertx_config:/app/config:rw #short-form volume (no /path is a short volume) + - netalertx_db:/app/db:rw +... +``` + +**After (Using a Local Folder / Bind Mount):** +Make sure to replace `/home/adam/netalertx-files` with your actual path. The format is `::`. + +``` +... + volumes: +# - netalertx_config:/app/config:rw +# - netalertx_db:/app/db:rw + - /home/adam/netalertx-files/config:/app/config:rw + - /home/adam/netalertx-files/db:/app/db:rw +... +``` + +Now, any files created by NetAlertX in `/app/config` will appear in your `/home/adam/netalertx-files/config` folder. + +This same method works for mounting other things, like custom plugins or enterprise NGINX files, as shown in the commented-out examples in the baseline file. + +## Example Configuration Summaries + +Here are the essential modifications for common alternative setups. + +### Example 2: External `.env` File for Paths + +This method is useful for keeping your paths and other settings separate from your main compose file, making it more portable. + +**`docker-compose.yml` changes:** + +``` +... services: netalertx: - container_name: netalertx - # use the below line if you want to test the latest dev image - # image: "ghcr.io/jokob-sk/netalertx-dev:latest" - image: "ghcr.io/jokob-sk/netalertx:latest" - network_mode: "host" - restart: unless-stopped - volumes: - - ${APP_CONFIG_LOCATION}/netalertx/config:/app/config - - ${APP_DATA_LOCATION}/netalertx/db/:/app/db/ - # (optional) useful for debugging if you have issues setting up the container - - ${LOGS_LOCATION}:/app/log - # (API: OPTION 1) use for performance - - type: tmpfs - target: /app/api - # (API: OPTION 2) use when debugging issues - # - local/path/api:/app/api environment: - - TZ=${TZ} + - TZ=${TZ} - PORT=${PORT} + +... ``` -`.env` file - -```yaml -#GLOBAL PATH VARIABLES - -APP_DATA_LOCATION=/path/to/docker_appdata -APP_CONFIG_LOCATION=/path/to/docker_config -LOGS_LOCATION=/path/to/docker_logs - -#ENVIRONMENT VARIABLES +**`.env` file contents:** +``` TZ=Europe/Paris PORT=20211 - -#DEVELOPMENT VARIABLES - -DEV_LOCATION=/path/to/local/source/code +NETALERTX_NETWORK_MODE=host +LISTEN_ADDR=0.0.0.0 +PORT=20211 +GRAPHQL_PORT=20212 ``` -To run the container execute: `sudo docker-compose --env-file /path/to/.env up` +Run with: `sudo docker-compose --env-file /path/to/.env up` +### Example 3: Docker Swarm -### Example 4: Docker swarm +This is for deploying on a Docker Swarm cluster. The key differences from the baseline are the removal of `network_mode:` from the service, and the addition of `deploy:` and `networks:` blocks at both the service and top-level. -Notice how the host network is defined in a swarm setup: +Here are the *only* changes you need to make to the baseline compose file to make it Swarm-compatible. -```yaml +``` services: netalertx: - # Use the below line if you want to test the latest dev image - # image: "jokobsk/netalertx-dev:latest" - image: "ghcr.io/jokob-sk/netalertx:latest" - volumes: - - /mnt/MYSERVER/netalertx/config:/config:rw - - /mnt/MYSERVER/netalertx/db:/netalertx/db:rw - - /mnt/MYSERVER/netalertx/logs:/netalertx/front/log:rw - environment: - - TZ=Europe/London - - PORT=20211 + ... + # network_mode: ${NETALERTX_NETWORK_MODE:-host} # <-- DELETE THIS LINE + ... + + # 2. ADD a 'networks:' block INSIDE the service to connect to the external host network. networks: - outside + # 3. ADD a 'deploy:' block to manage the service as a swarm replica. deploy: mode: replicated replicas: 1 restart_policy: condition: on-failure + +# 4. ADD a new top-level 'networks:' block at the end of the file to define 'outside' as the external 'host' network. networks: outside: external: name: "host" - - -``` - -### Example 5: same as 3 but with a top-level root directory; also works in Portainer as-is - -`docker-compose.yml` - -```yaml -services: - netalertx: - container_name: netalertx - # use the below line if you want to test the latest dev image instead of the stable release - # image: "ghcr.io/jokob-sk/netalertx-dev:latest" - image: "ghcr.io/jokob-sk/netalertx:latest" - - network_mode: "host" - restart: unless-stopped - volumes: - - ${APP_FOLDER}/netalertx/config:/app/config - - ${APP_FOLDER}/netalertx/db:/app/db - # (optional) useful for debugging if you have issues setting up the container - - ${APP_FOLDER}/netalertx/log:/app/log - # (API: OPTION 1) default -> use for performance - - type: tmpfs - target: /app/api - # (API: OPTION 2) use when debugging issues - # - ${APP_FOLDER}/netalertx/api:/app/api - environment: - - - TZ=${TZ} - - PORT=${PORT} - - PUID=${PUID} - - PGID=${PGID} - - LISTEN_ADDR=${LISTEN_ADDR} -``` - -`.env` file - -```yaml -APP_FOLDER=/path/to/local/NetAlertX/location - -#ENVIRONMENT VARIABLES - -PUID=200 -PGID=300 - -TZ=America/New_York -LISTEN_ADDR=0.0.0.0 -PORT=20211 -#GLOBAL PATH VARIABLE - -# you may want to create a dedicated user and group to run the container with -# sudo groupadd -g 300 nax-g -# sudo useradd -u 200 -g 300 nax-u -# mkdir -p $APP_FOLDER/{db,config,log} -# chown -R 200:300 $APP_FOLDER -# chmod -R 775 $APP_FOLDER - -# DEVELOPMENT VARIABLES -# you can create multiple env files called .env.dev1, .env.dev2 etc and use them by running: -# docker compose --env-file .env.dev1 up -d -# you can then clone multiple dev copies of NetAlertX just make sure to change the APP_FOLDER and PORT variables in each .env.devX file - -``` - -To run the container execute: `sudo docker-compose --env-file /path/to/.env up` +``` \ No newline at end of file From fba53598393cf5401f53b3ba5ae4f9fbfdc465fc Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 30 Oct 2025 13:14:06 +1100 Subject: [PATCH 11/28] DOCS: Docker guides Signed-off-by: jokob-sk --- docs/FILE_PERMISSIONS.md | 94 ++++++++++++++++++++++----------- docs/MIGRATION.md | 109 +++++++++++++++++++++++++-------------- 2 files changed, 134 insertions(+), 69 deletions(-) diff --git a/docs/FILE_PERMISSIONS.md b/docs/FILE_PERMISSIONS.md index 42ccd603..c6ceaafc 100755 --- a/docs/FILE_PERMISSIONS.md +++ b/docs/FILE_PERMISSIONS.md @@ -1,46 +1,78 @@ -# Managing File Permissions for NetAlertX on Nginx with Docker +# Managing File Permissions for NetAlertX on a Read-Only Container > [!TIP] -> If you are facing permission issues, try to start the container without mapping your volumes. If that works, then the issue is permission related. You can try e.g., the following command: -> ``` -> docker run -d --rm --network=host \ -> -e TZ=Europe/Berlin \ -> -e PUID=200 -e PGID=200 \ -> -e PORT=20211 \ -> ghcr.io/jokob-sk/netalertx:latest -> ``` -NetAlertX runs on an Nginx web server. On Alpine Linux, Nginx operates as the `nginx` user (if PUID and GID environment variables are not specified, nginx user UID will be set to 102, and its supplementary group `www-data` ID to 82). Consequently, files accessed or written by the NetAlertX application are owned by `nginx:www-data`. +> NetAlertX runs in a **secure, read-only Alpine-based container** under a dedicated `netalertx` user (UID 20211, GID 20211). All writable paths are either mounted as **persistent volumes** or **`tmpfs` filesystems**. This ensures consistent file ownership and prevents privilege escalation. -Upon starting, NetAlertX changes nginx user UID and www-data GID to specified values (or defaults), and the ownership of files on the host system mapped to `/app/config` and `/app/db` in the container to `nginx:www-data`. This ensures that Nginx can access and write to these files. Since the user in the Docker container is mapped to a user on the host system by ID:GID, the files in `/app/config` and `/app/db` on the host system are owned by a user with the same ID and GID (defaults are ID 102 and GID 82). On different systems, this ID:GID may belong to different users, or there may not be a group with ID 82 at all. +--- -Option to set specific user UID and GID can be useful for host system users needing to access these files (e.g., backup scripts). +## Writable Paths -### Permissions Table for Individual Folders +NetAlertX requires certain paths to be writable at runtime. These paths should be mounted either as **host volumes** or **`tmpfs`** in your `docker-compose.yml` or `docker run` command: -| Folder | User | User ID | Group | Group ID | Permissions | Notes | -|----------------|--------|---------|-----------|----------|-------------|---------------------------------------------------------------------| -| `/app/config` | nginx | PUID (default 102) | www-data | PGID (default 82) | rwxr-xr-x | Ensure `nginx` can read/write; other users can read if in `www-data` | -| `/app/db` | nginx | PUID (default 102) | www-data | PGID (default 82) | rwxr-xr-x | Same as above | +| Path | Purpose | Notes | +| ------------------------------------ | ----------------------------------- | ------------------------------------------------------ | +| `/app/config` | Application configuration | Persistent volume recommended | +| `/app/db` | Database files | Persistent volume recommended | +| `/app/log` | Logs | Can be `tmpfs` for speed or host volume to retain logs | +| `/app/api` | API cache | Use `tmpfs` for faster access | +| `/services/config/nginx/conf.active` | Active nginx configuration override | `tmpfs` recommended or customiozed file mounted | +| `/services/run` | Runtime directories for nginx & PHP | `tmpfs` required | +| `/tmp` | PHP session save directory | `tmpfs` required | +> All these paths will have **UID 20211 / GID 20211** inside the container. Files on the host will appear owned by `20211:20211`. -### Fixing Permission Problems +--- -The container fails to start with "Permission Denied" errors. This typically happens when your data volumes (`netalertx_config`, `netalertx_db`) are "owned" by the `root` user (UID 0) from a previous install, but the secure container must run as the `netalertx` user (UID 20211). +## Fixing Permission Problems + +Sometimes, permission issues arise if your existing host directories were created by a previous container running as root or another UID. The container will fail to start with "Permission Denied" errors. ### Solution -1. Run the container **once** as the `root` user (`--user "0"`) to trigger the built-in fix-it mode: +1. **Run the container once as root** (`--user "0"`) to allow it to correct permissions automatically: - ```bash - docker run -it --rm --name netalertx-fix --user "0" \ - -v netalertx_config:/app/config \ - -v netalertx_db:/app/db \ - netalertx:latest - ``` -2. Wait for the logs to show a **magenta warning** and confirm permissions are being fixed. The container will then hang (this is intentional). -3. Press **Ctrl+C** to stop the fix-it container. -4. Start your container normally (e.g., with `docker-compose up -d` or your standard `docker run` command). +```bash +docker run -it --rm --name netalertx --user "0" \ + -v local/path/config:/app/config \ + -v local/path/db:/app/db \ + ghcr.io/jokob-sk/netalertx:latest +``` + +2. Wait for logs showing **permissions being fixed**. The container will then **hang intentionally**. +3. Press **Ctrl+C** to stop the container. +4. Start the container normally with your `docker-compose.yml` or `docker run` command. + +> The container startup script detects `root` and runs `chown -R 20211:20211` on all volumes, fixing ownership for the secure `netalertx` user. + +--- + +## Example: docker-compose.yml with `tmpfs` + +```yaml +services: + netalertx: + container_name: netalertx + image: "ghcr.io/jokob-sk/netalertx" + network_mode: "host" + cap_add: + - NET_RAW + - NET_ADMIN + - NET_BIND_SERVICE + restart: unless-stopped + volumes: + - local/path/config:/app/config + - local/path/db:/app/db + environment: + - TZ=Europe/Berlin + - PORT=20211 + tmpfs: + - "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + - "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,sync,noatime,nodiratime" + - "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + - "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + - "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" +``` + +> This setup ensures all writable paths are either in `tmpfs` or host-mounted, and the container never writes outside of controlled volumes. -### About This Method -The container’s startup script detects it is running as `root` (UID 0). It automatically runs the `chown` command to fix the permissions on your volume files, setting them to the correct `20211` user. It then hangs (`sleep infinity`) to prevent you from *ever* running the application as `root`, forcing you to restart securely. \ No newline at end of file diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 0d0da165..07984177 100755 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,6 +1,6 @@ # Migration -When upgrading from older versions of NetAlertX (or PiAlert (by jokob-sk)) the following data and setup migration steps need to be followed. +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] > It's always important to have a [backup strategy](./BACKUPS.md) in place. @@ -27,7 +27,7 @@ You can migrate data manually, for example by exporting and importing devices us #### STEPS: The application will automatically migrate the database, configuration, and all device information. -A ticker message will appear at the top of the web UI until you update your Docker mount points. +A banner message will appear at the top of the web UI reminding you to update your Docker mount points. 1. Stop the container 2. [Back up your setup](./BACKUPS.md) @@ -37,7 +37,7 @@ A ticker message will appear at the top of the web UI until you update your Dock > [!TIP] -> If you have troubles accessing past backups, config or database files you can copy them into the newly mapped directories, for example by running this command in the container: `cp -r /app/config /home/pi/pialert/config/old_backup_files`. This should create a folder in the `config` directory called `old_backup_files` containing all the files in that location. Another approach is to map the old location and the new one at the same time to copy things over. +> If you have trouble accessing past backups, config or database files you can copy them into the newly mapped directories, for example by running this command in the container: `cp -r /app/config /home/pi/pialert/config/old_backup_files`. This should create a folder in the `config` directory called `old_backup_files` containing all the files in that location. Another approach is to map the old location and the new one at the same time to copy things over. #### New Docker mount locations @@ -58,7 +58,7 @@ The internal application path in the container has changed from `/home/pi/pialer > [!NOTE] -> The application uses symlinks linking the old db and config locations to the new ones, so data loss should not occur. [Backup strategies](./BACKUPS.md) are still recommended to backup your setup. +> The application automatically creates symlinks from the old database and config locations to the new ones, so data loss should not occur. Read the [backup strategies](./BACKUPS.md) guide to backup your setup. #### Examples @@ -92,16 +92,16 @@ services: ```yaml services: - netalertx: # ⚠ This has changed (🟡optional) - container_name: netalertx # ⚠ This has changed (🟡optional) - image: "ghcr.io/jokob-sk/netalertx:25.5.24" # ⚠ This has changed (🟡optional/🔺required in future) + netalertx: # 🆕 This has changed + container_name: netalertx # 🆕 This has changed + image: "ghcr.io/jokob-sk/netalertx:25.5.24" # 🆕 This has changed network_mode: "host" restart: unless-stopped volumes: - - local/path/config:/app/config # ⚠ This has changed (🔺required) - - local/path/db:/app/db # ⚠ This has changed (🔺required) + - local/path/config:/app/config # 🆕 This has changed + - local/path/db:/app/db # 🆕 This has changed # (optional) useful for debugging if you have issues setting up the container - - local/path/logs:/app/log # ⚠ This has changed (🟡optional) + - local/path/logs:/app/log # 🆕 This has changed environment: - TZ=Europe/Berlin - PORT=20211 @@ -138,16 +138,16 @@ services: ```yaml services: - netalertx: # ⚠ This has changed (🟡optional) - container_name: netalertx # ⚠ This has changed (🟡optional) - image: "ghcr.io/jokob-sk/netalertx:25.5.24" # ⚠ This has changed (🟡optional/🔺required in future) + netalertx: # 🆕 This has changed + container_name: netalertx # 🆕 This has changed + image: "ghcr.io/jokob-sk/netalertx:25.5.24" # 🆕 This has changed network_mode: "host" restart: unless-stopped volumes: - - local/path/config/app.conf:/app/config/app.conf # ⚠ This has changed (🔺required) - - local/path/db/app.db:/app/db/app.db # ⚠ This has changed (🔺required) + - local/path/config/app.conf:/app/config/app.conf # 🆕 This has changed + - local/path/db/app.db:/app/db/app.db # 🆕 This has changed # (optional) useful for debugging if you have issues setting up the container - - local/path/logs:/app/log # ⚠ This has changed (🟡optional) + - local/path/logs:/app/log # 🆕 This has changed environment: - TZ=Europe/Berlin - PORT=20211 @@ -156,7 +156,7 @@ services: ### 1.2 Migration from NetAlertX `v25.5.24` -Versions before `v25.10.1` require an intermediate migration through `v25.5.24` to ensure database compatibility. +Versions before `v25.10.1` require an intermediate migration through `v25.5.24` to ensure database compatibility. Skipping this step may cause compatibility issues due to database schema changes introduced after `v25.5.24`. #### STEPS: @@ -180,7 +180,7 @@ Examples of docker files with the tagged version. services: netalertx: container_name: netalertx - image: "ghcr.io/jokob-sk/netalertx:25.5.24" # ⚠ This is important (🔺required) + image: "ghcr.io/jokob-sk/netalertx:25.5.24" # 🆕 This is important network_mode: "host" restart: unless-stopped volumes: @@ -197,7 +197,7 @@ services: services: netalertx: container_name: netalertx - image: "ghcr.io/jokob-sk/netalertx:25.10.1" # ⚠ This is important (🔺required) + image: "ghcr.io/jokob-sk/netalertx:25.10.1" # 🆕 This is important network_mode: "host" restart: unless-stopped volumes: @@ -212,30 +212,19 @@ services: ### 1.3 Migration from NetAlertX `v25.10.1` -> [!WARNING] -> This section is under development. The migration path from `v25.10.1` to future versions (e.g., `v25.11.x` and newer) will be published soon. +Starting from v25.10.1, the container uses a [more secure, read-only runtime environment](./SECURITY_FEATURES.md), which requires all writable paths (e.g., logs, API cache, temporary data) to be mounted as `tmpfs` or permanent writable volumes, with sufficient access [permissions](./FILE_PERMISSIONS.md). #### STEPS: 1. Stop the container 2. [Back up your setup](./BACKUPS.md) -3. Upgrade to `v25.10.1` by pinning the release version (See Examples below) -4. Start the container and verify everything works as expected. -5. Stop the container -6. 🔻 TBC 🔺 Perform a one-off migration to the `20211` user `docker run -it --rm --name netalertx --user "0" -v netalertx_config:/app/config -v netalertx_db:/app/db netalertx:latest` -7. 🔻 TBC 🔺 Stop the container -8. 🔻 TBC 🔺 Switch to the latest `netalertx` image -9. 🔻 TBC 🔺 Start the container and verify everything works as expected. - -##### Example 1: Mapping folders - -###### docker-compose.yml changes +3. Upgrade to `v25.10.1` by pinning the release version (See the example below) ```yaml services: netalertx: container_name: netalertx - image: "ghcr.io/jokob-sk/netalertx:25.10.1" # ⚠ This is important (🔺required) + image: "ghcr.io/jokob-sk/netalertx:25.10.1" # 🆕 This is important network_mode: "host" restart: unless-stopped volumes: @@ -248,12 +237,56 @@ services: - PORT=20211 ``` -🔻 TBC 🔺 +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: -```bash +> [!NOTE] +> The example below assumes your `/config` and `/db` folders are stored in `local/path`. +> Replace this path with your actual configuration directory. `netalertx` is the container name, whcih might differ from your setup. + +```sh docker run -it --rm --name netalertx --user "0" \ - -v netalertx_config:/app/config \ - -v netalertx_db:/app/db \ - netalertx:latest + -v local/path/config:/app/config \ + -v local/path/db:/app/db \ + ghcr.io/jokob-sk/netalertx:latest ``` +7. Stop the container +8. Update the `docker-compose.yml` as per example below. + +```yaml +services: + netalertx: + container_name: netalertx + image: "ghcr.io/jokob-sk/netalertx" # 🆕 This is important + network_mode: "host" + cap_add: # 🆕 New line + - NET_RAW # 🆕 New line + - NET_ADMIN # 🆕 New line + - NET_BIND_SERVICE # 🆕 New line + restart: unless-stopped + volumes: + - local/path/config:/app/config + - local/path/db:/app/db + # (optional) useful for debugging if you have issues setting up the container + #- local/path/logs:/app/log + environment: + - TZ=Europe/Berlin + - PORT=20211 + # 🆕 New "tmpfs" section START 🔽 + tmpfs: + # Speed up logging. This can be commented out to retain logs between container restarts + - "/app/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # Speed up API access as frontend/backend API is very chatty + - "/app/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,sync,noatime,nodiratime" + # Required for customization of the nginx listen addr/port without rebuilding the container + - "/services/config/nginx/conf.active:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # /services/config/nginx/conf.d is required for nginx and php to start + - "/services/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # /tmp is required by php for session save this should be reworked to /services/run/tmp + - "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # 🆕 New "tmpfs" section END 🔼 +``` + +9. Start the container and verify everything works as expected. \ No newline at end of file From 50f9277e5ed14cc92700f3675c1dc497c73f837d Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 30 Oct 2025 13:30:23 +1100 Subject: [PATCH 12/28] DOCS: Docker guides (GRAPHQL_PORT fix) Signed-off-by: jokob-sk --- docs/DOCKER_COMPOSE.md | 2 +- docs/PIHOLE_GUIDE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DOCKER_COMPOSE.md b/docs/DOCKER_COMPOSE.md index 77e98a28..6b595689 100755 --- a/docs/DOCKER_COMPOSE.md +++ b/docs/DOCKER_COMPOSE.md @@ -79,7 +79,7 @@ services: environment: LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces PORT: ${PORT:-20211} # Application port - APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212} # GraphQL API port (passed as APP_CONF_OVERRIDE) + GRAPHQL_PORT: ${GRAPHQL_PORT:-20212} # GraphQL API port (passed into APP_CONF_OVERRIDE at runtime) NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} # 0=kill all services and restart if any dies. 1 keeps running dead services. # Resource limits to prevent resource exhaustion diff --git a/docs/PIHOLE_GUIDE.md b/docs/PIHOLE_GUIDE.md index 62164667..4b2f703e 100755 --- a/docs/PIHOLE_GUIDE.md +++ b/docs/PIHOLE_GUIDE.md @@ -1,6 +1,6 @@ # Integration with PiHole -NetAlertX comes with 2 plugins suitable for integrating with your existing PiHole instace. 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 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). ## Approach 1: `DHCPLSS` Plugin - Import devices from the PiHole DHCP leases file From 8da136f19217b3dea0e004a243f5431b5df4ca47 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 30 Oct 2025 13:55:05 +1100 Subject: [PATCH 13/28] BE: Remove GraphQL check from healthcheck Signed-off-by: jokob-sk --- .../production-filesystem/services/healthcheck.sh | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/install/production-filesystem/services/healthcheck.sh b/install/production-filesystem/services/healthcheck.sh index 81c0a9fa..bfe1930f 100755 --- a/install/production-filesystem/services/healthcheck.sh +++ b/install/production-filesystem/services/healthcheck.sh @@ -55,13 +55,14 @@ else log_error "Port ${PORT:-20211} is not responding or doesn't contain 'netalertx'" fi -# 6. Check port 20212/graphql returns "graphql" in first lines -GRAPHQL_PORT=${GRAPHQL_PORT:-20212} -if curl -sf --max-time 10 "http://localhost:${GRAPHQL_PORT}/graphql" | head -10 | grep -i "graphql" > /dev/null; then - log_success "Port ${GRAPHQL_PORT}/graphql is responding with GraphQL content" -else - log_error "Port ${GRAPHQL_PORT}/graphql is not responding or doesn't contain 'graphql'" -fi +# NOTE: GRAPHQL_PORT might not be set and is initailized as a setting with a default value in the container. It can also be initialized via APP_CONF_OVERRIDE +# # 6. Check port 20212/graphql returns "graphql" in first lines +# GRAPHQL_PORT=${GRAPHQL_PORT:-20212} +# if curl -sf --max-time 10 "http://localhost:${GRAPHQL_PORT}/graphql" | head -10 | grep -i "graphql" > /dev/null; then +# log_success "Port ${GRAPHQL_PORT}/graphql is responding with GraphQL content" +# else +# log_error "Port ${GRAPHQL_PORT}/graphql is not responding or doesn't contain 'graphql'" +# fi # Summary if [ $EXIT_CODE -eq 0 ]; then From f81a1b93f9cf98274c88fbcc85cd95c3c08b0175 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 30 Oct 2025 14:31:22 +1100 Subject: [PATCH 14/28] DOCS: Docker guides Signed-off-by: jokob-sk --- docs/DOCKER_COMPOSE.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/DOCKER_COMPOSE.md b/docs/DOCKER_COMPOSE.md index 6b595689..aafe96b9 100755 --- a/docs/DOCKER_COMPOSE.md +++ b/docs/DOCKER_COMPOSE.md @@ -3,13 +3,13 @@ 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.s operating system. +> 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. -``` +```yaml services: netalertx: #use an environmental variable to set host networking mode if needed @@ -104,7 +104,7 @@ volumes: # Persistent volumes for configuration and datab Run or re-run it: -``` +```sh docker compose up --force-recreate ``` @@ -116,7 +116,7 @@ You can override the default settings by passing environmental variables to the This command runs NetAlertX on port 8080 instead of the default 20211. -``` +```sh PORT=8080 docker compose up ``` @@ -124,7 +124,7 @@ PORT=8080 docker compose up This command demonstrates overriding all primary environmental variables: running with host networking, on port 20211, GraphQL on 20212, and listening on all IPs. -``` +```sh NETALERTX_NETWORK_MODE=host \ LISTEN_ADDR=0.0.0.0 \ PORT=20211 \ @@ -155,7 +155,7 @@ However, if you prefer to have direct, file-level access to your configuration f **Before (Using Named Volumes - Preferred):** -``` +```yaml ... volumes: - netalertx_config:/app/config:rw #short-form volume (no /path is a short volume) @@ -166,7 +166,7 @@ However, if you prefer to have direct, file-level access to your configuration f **After (Using a Local Folder / Bind Mount):** Make sure to replace `/home/adam/netalertx-files` with your actual path. The format is `::`. -``` +```yaml ... volumes: # - netalertx_config:/app/config:rw @@ -190,7 +190,7 @@ This method is useful for keeping your paths and other settings separate from yo **`docker-compose.yml` changes:** -``` +```yaml ... services: netalertx: @@ -203,7 +203,7 @@ services: **`.env` file contents:** -``` +```sh TZ=Europe/Paris PORT=20211 NETALERTX_NETWORK_MODE=host @@ -220,7 +220,7 @@ This is for deploying on a Docker Swarm cluster. The key differences from the ba Here are the *only* changes you need to make to the baseline compose file to make it Swarm-compatible. -``` +```yaml services: netalertx: ... From 869f28b036597e884b09e32c159881882fde41de Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 30 Oct 2025 14:50:13 +1100 Subject: [PATCH 15/28] DOCS: typos Signed-off-by: jokob-sk --- docs/FILE_PERMISSIONS.md | 2 +- docs/MIGRATION.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/FILE_PERMISSIONS.md b/docs/FILE_PERMISSIONS.md index c6ceaafc..56f57bd4 100755 --- a/docs/FILE_PERMISSIONS.md +++ b/docs/FILE_PERMISSIONS.md @@ -15,7 +15,7 @@ NetAlertX requires certain paths to be writable at runtime. These paths should b | `/app/db` | Database files | Persistent volume recommended | | `/app/log` | Logs | Can be `tmpfs` for speed or host volume to retain logs | | `/app/api` | API cache | Use `tmpfs` for faster access | -| `/services/config/nginx/conf.active` | Active nginx configuration override | `tmpfs` recommended or customiozed file mounted | +| `/services/config/nginx/conf.active` | Active nginx configuration override | `tmpfs` recommended or customized file mounted | | `/services/run` | Runtime directories for nginx & PHP | `tmpfs` required | | `/tmp` | PHP session save directory | `tmpfs` required | diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 07984177..84aa55cc 100755 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -243,7 +243,7 @@ services: > [!NOTE] > The example below assumes your `/config` and `/db` folders are stored in `local/path`. -> Replace this path with your actual configuration directory. `netalertx` is the container name, whcih might differ from your setup. +> 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" \ From 274becab9789659f4d21e00478ead3fcbad2901b Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 30 Oct 2025 14:51:24 +1100 Subject: [PATCH 16/28] BE: fix GRAPHQL_PORT Signed-off-by: jokob-sk --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c5485fdd..4ef7daeb 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,10 +67,10 @@ services: - "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" environment: LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces - PORT: ${PORT:-20211} # Application port - APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212} # GraphQL API port - ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false} # Set to true to reset your config and database on each container start - NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} # 0=kill all services and restart if any dies. 1 keeps running dead services. + PORT: ${PORT:-20211} # Application port + GRAPHQL_PORT: ${GRAPHQL_PORT:-20212} # GraphQL API port + ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false} # Set to true to reset your config and database on each container start + NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} # 0=kill all services and restart if any dies. 1 keeps running dead services. # Resource limits to prevent resource exhaustion mem_limit: 2048m # Maximum memory usage From 512dedff4e4c1c9e22796469a104fca125f280cc Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 06:39:55 +1100 Subject: [PATCH 17/28] FE: increase filter debounce to 750ms #1254 Signed-off-by: jokob-sk --- front/devices.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/devices.php b/front/devices.php index e6ed8261..daa3a67c 100755 --- a/front/devices.php +++ b/front/devices.php @@ -990,7 +990,7 @@ function initializeDatatable (status) { // search only after idle var typingTimer; // Timer identifier - var debounceTime = 500; // Delay in milliseconds + var debounceTime = 750; // Delay in milliseconds $('input[aria-controls="tableDevices"]').off().on('keyup', function () { clearTimeout(typingTimer); // Clear the previous timer From 929eb1626b13ad6a89ed942140dadd6c1051cc0d Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:48:38 +0000 Subject: [PATCH 18/28] BE: Devices Tiles SQL syntax error #1238 --- server/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/const.py b/server/const.py index 8ec7fbd2..9149a0bd 100755 --- a/server/const.py +++ b/server/const.py @@ -107,8 +107,8 @@ sql_devices_tiles = """ (SELECT COUNT(*) FROM Devices WHERE devIsNew = 1) AS new, (SELECT COUNT(*) FROM Devices WHERE devIsArchived = 1) AS archived, (SELECT COUNT(*) FROM Devices WHERE devFavorite = 1) AS favorites, - (SELECT COUNT(*) FROM Devices) AS all, - (SELECT COUNT(*) FROM Devices) AS all_devices, + (SELECT COUNT(*) FROM Devices) AS "all", + (SELECT COUNT(*) FROM Devices) AS "all_devices", -- My Devices count (SELECT COUNT(*) FROM MyDevicesFilter) AS my_devices FROM Statuses; From 63d6410bb4744437d51fb9ab196d26fa3bbe8ef6 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 08:12:38 +1100 Subject: [PATCH 19/28] BE: handle missing buildtimestamp.txt Signed-off-by: jokob-sk --- server/helper.py | 12 ++++++++++-- server/initialise.py | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/server/helper.py b/server/helper.py index 263ac5b2..bb74ef5a 100755 --- a/server/helper.py +++ b/server/helper.py @@ -773,8 +773,16 @@ def checkNewVersion(): newVersion = False - with open(applicationPath + '/front/buildtimestamp.txt', 'r') as f: - buildTimestamp = int(f.read().strip()) + build_timestamp_path = os.path.join(applicationPath, 'front/buildtimestamp.txt') + + # Ensure file exists, initialize if missing + if not os.path.exists(build_timestamp_path): + with open(build_timestamp_path, 'w') as f: + f.write("0") + + # Now safely read the timestamp + with open(build_timestamp_path, 'r') as f: + buildTimestamp = int(f.read().strip() or 0) try: response = requests.get( diff --git a/server/initialise.py b/server/initialise.py index 52e09d12..ac8062a1 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -378,7 +378,14 @@ def importConfigs (pm, db, all_plugins): # HANDLE APP was upgraded message - clear cache # Check if app was upgraded - with open(applicationPath + '/front/buildtimestamp.txt', 'r') as f: + build_timestamp_path = os.path.join(applicationPath, 'front/buildtimestamp.txt') + + # Ensure the build timestamp file exists and has an initial value + if not os.path.exists(build_timestamp_path): + with open(build_timestamp_path, 'w') as f: + f.write("0") + + with open(build_timestamp_path, 'r') as f: buildTimestamp = int(f.read().strip()) cur_version = conf.VERSION From 75072dad5fc8ec5e9a6924c1140b3bc1e695c76c Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 08:16:54 +1100 Subject: [PATCH 20/28] GIT: build dev container from next_release branch Signed-off-by: jokob-sk --- .github/workflows/docker_dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_dev.yml b/.github/workflows/docker_dev.yml index 3a4ed6c5..08c4bdba 100755 --- a/.github/workflows/docker_dev.yml +++ b/.github/workflows/docker_dev.yml @@ -3,12 +3,12 @@ name: docker on: push: branches: - - main + - next_release tags: - '*.*.*' pull_request: branches: - - main + - next_release jobs: docker_dev: From 2f7d9a02ae564b275b7262563a97e5fcec5b7355 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 15:02:51 +1100 Subject: [PATCH 21/28] PLG: snmpwalk -OXsq clarification #1231 --- front/plugins/snmp_discovery/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/plugins/snmp_discovery/config.json b/front/plugins/snmp_discovery/config.json index e82750c8..9b6dce62 100755 --- a/front/plugins/snmp_discovery/config.json +++ b/front/plugins/snmp_discovery/config.json @@ -483,7 +483,7 @@ "description": [ { "language_code": "en_us", - "string": "A list of snmpwalk commands to execute against IP addresses of roputers/switches with SNMP turned on.

Example with the router on the IP 192.168.1.1:
snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2

Only IPv4 supported. Authentication is not supported. More info on the plugin here." + "string": "A list of snmpwalk commands to execute against IP addresses of roputers/switches with SNMP turned on.

Example with the router on the IP 192.168.1.1 (the -OXsq is a required parameter):
snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2

Only IPv4 supported. Authentication is not supported. More info on the plugin here." }, { "language_code": "es_es", From 64e4586be6ba2345d92a2eb954c4e854e14d1d23 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 20:24:13 +1100 Subject: [PATCH 22/28] PLG: Encode SMTP_PASS using base64 #1253 --- front/plugins/_publisher_email/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/plugins/_publisher_email/config.json b/front/plugins/_publisher_email/config.json index 387c8d7a..ed147e61 100755 --- a/front/plugins/_publisher_email/config.json +++ b/front/plugins/_publisher_email/config.json @@ -534,7 +534,7 @@ { "elementType": "input", "elementOptions": [{ "type": "password" }], - "transformers": [] + "transformers": ["base64"] } ] }, From 78ab0fbd2db536aa81989141bd549fa4420266a7 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 20:24:13 +1100 Subject: [PATCH 23/28] PLG: SNMPDSC typo --- front/plugins/_publisher_email/config.json | 2 +- front/plugins/snmp_discovery/config.json | 2 +- server/helper.py | 24 +++++++++++++-- server/initialise.py | 34 +++++++++------------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/front/plugins/_publisher_email/config.json b/front/plugins/_publisher_email/config.json index 387c8d7a..ed147e61 100755 --- a/front/plugins/_publisher_email/config.json +++ b/front/plugins/_publisher_email/config.json @@ -534,7 +534,7 @@ { "elementType": "input", "elementOptions": [{ "type": "password" }], - "transformers": [] + "transformers": ["base64"] } ] }, diff --git a/front/plugins/snmp_discovery/config.json b/front/plugins/snmp_discovery/config.json index 9b6dce62..19f63233 100755 --- a/front/plugins/snmp_discovery/config.json +++ b/front/plugins/snmp_discovery/config.json @@ -483,7 +483,7 @@ "description": [ { "language_code": "en_us", - "string": "A list of snmpwalk commands to execute against IP addresses of roputers/switches with SNMP turned on.

Example with the router on the IP 192.168.1.1 (the -OXsq is a required parameter):
snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2

Only IPv4 supported. Authentication is not supported. More info on the plugin here." + "string": "A list of snmpwalk commands to execute against IP addresses of routers/switches with SNMP turned on.

Example with the router on the IP 192.168.1.1 (the -OXsq is a required parameter):
snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2

Only IPv4 supported. Authentication is not supported. More info on the plugin here." }, { "language_code": "es_es", diff --git a/server/helper.py b/server/helper.py index bb74ef5a..e48958b8 100755 --- a/server/helper.py +++ b/server/helper.py @@ -768,11 +768,19 @@ def collect_lang_strings(json, pref, stringSqlParams): return stringSqlParams #------------------------------------------------------------------------------- -def checkNewVersion(): - mylog('debug', [f"[Version check] Checking if new version available"]) +# Get the value from the buildtimestamp.txt and initialize it if missing +def getBuildTimeStamp(): + """ + Retrieves the build timestamp from 'front/buildtimestamp.txt' within the + application directory. - newVersion = False + If the file does not exist, it is created and initialized with the value '0'. + Returns: + int: The integer value of the build timestamp read from the file. + Returns 0 if the file is empty or just initialized. + """ + buildTimestamp = 0 build_timestamp_path = os.path.join(applicationPath, 'front/buildtimestamp.txt') # Ensure file exists, initialize if missing @@ -784,6 +792,16 @@ def checkNewVersion(): with open(build_timestamp_path, 'r') as f: buildTimestamp = int(f.read().strip() or 0) + return buildTimestamp + + +#------------------------------------------------------------------------------- +def checkNewVersion(): + mylog('debug', [f"[Version check] Checking if new version available"]) + + newVersion = False + buildTimestamp = getBuildTimeStamp() + try: response = requests.get( "https://api.github.com/repos/jokob-sk/NetAlertX/releases", diff --git a/server/initialise.py b/server/initialise.py index ac8062a1..0a8fa82d 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -12,7 +12,7 @@ import re # Register NetAlertX libraries import conf from const import fullConfPath, applicationPath, fullConfFolder, default_tz -from helper import fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowTZ, get_setting_value, generate_random_string +from helper import getBuildTimeStamp, fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowTZ, get_setting_value, generate_random_string from app_state import updateState from logger import mylog from api import update_api @@ -378,29 +378,21 @@ def importConfigs (pm, db, all_plugins): # HANDLE APP was upgraded message - clear cache # Check if app was upgraded - build_timestamp_path = os.path.join(applicationPath, 'front/buildtimestamp.txt') - - # Ensure the build timestamp file exists and has an initial value - if not os.path.exists(build_timestamp_path): - with open(build_timestamp_path, 'w') as f: - f.write("0") - - with open(build_timestamp_path, 'r') as f: - buildTimestamp = int(f.read().strip()) - cur_version = conf.VERSION + buildTimestamp = getBuildTimeStamp() + cur_version = conf.VERSION + + mylog('debug', [f"[Config] buildTimestamp: '{buildTimestamp}'"]) + mylog('debug', [f"[Config] conf.VERSION : '{cur_version}'"]) + + if str(cur_version) != str(buildTimestamp): - mylog('debug', [f"[Config] buildTimestamp: '{buildTimestamp}'"]) - mylog('debug', [f"[Config] conf.VERSION : '{cur_version}'"]) + mylog('none', ['[Config] App upgraded 🚀']) + + # ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", setJsonMetadata=None, overrideTemplate=None, forceDefault=False) + ccd('VERSION', buildTimestamp , c_d, '_KEEP_', '_KEEP_', '_KEEP_', '_KEEP_', None, "_KEEP_", None, None, True) - if str(cur_version) != str(buildTimestamp): - - mylog('none', ['[Config] App upgraded 🚀']) - - # ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", setJsonMetadata=None, overrideTemplate=None, forceDefault=False) - ccd('VERSION', buildTimestamp , c_d, '_KEEP_', '_KEEP_', '_KEEP_', '_KEEP_', None, "_KEEP_", None, None, True) - - write_notification(f'[Upgrade] : App upgraded 🚀 Please clear the cache:
  1. Click OK below
  2. Clear the browser cache (shift + browser refresh button)
  3. Clear app cache with the (reload) button in the header
  4. Go to Settings and click Save
Check out new features and what has changed in the 📓 release notes.', 'interrupt', timeNowTZ()) + write_notification(f'[Upgrade] : App upgraded 🚀 Please clear the cache:
  1. Click OK below
  2. Clear the browser cache (shift + browser refresh button)
  3. Clear app cache with the (reload) button in the header
  4. Go to Settings and click Save
Check out new features and what has changed in the 📓 release notes.', 'interrupt', timeNowTZ()) From bc9fb6bcde06f705369c25d5b3abc004d100fd71 Mon Sep 17 00:00:00 2001 From: jeet moh Date: Thu, 30 Oct 2025 13:07:48 +0100 Subject: [PATCH 24/28] Translated using Weblate (Persian (fa_FA)) Currently translated at 0.1% (1 of 762 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/fa_FA/ --- front/php/templates/language/fa_fa.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100755 => 100644 front/php/templates/language/fa_fa.json diff --git a/front/php/templates/language/fa_fa.json b/front/php/templates/language/fa_fa.json old mode 100755 new mode 100644 index d722c4cf..4c0f14e2 --- a/front/php/templates/language/fa_fa.json +++ b/front/php/templates/language/fa_fa.json @@ -1,6 +1,6 @@ { "API_CUSTOM_SQL_description": "", - "API_CUSTOM_SQL_name": "", + "API_CUSTOM_SQL_name": "مقصده سفارشی", "API_TOKEN_description": "", "API_TOKEN_name": "", "API_display_name": "", @@ -761,4 +761,4 @@ "settings_system_label": "", "settings_update_item_warning": "", "test_event_tooltip": "" -} \ No newline at end of file +} From 0b0899522324668f0a55541efbfd99e8832aebd4 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 21:46:25 +1100 Subject: [PATCH 25/28] Revert "DOCS: install refactor work" This reverts commit fe69972caa9ca8d5b711c366b71a64532cf168d3. --- .gitignore | 37 ++----- docs/INSTALLATION.md | 4 +- docs/INSTALLATION_DOCKER.md | 103 ------------------ .../@eaDir/device_details.png@SynoEAStream | Bin 0 -> 198 bytes docs/img/@eaDir/devices_dark.png@SynoEAStream | Bin 0 -> 198 bytes .../img/@eaDir/devices_light.png@SynoEAStream | Bin 0 -> 198 bytes .../img/@eaDir/devices_split.png@SynoEAStream | Bin 0 -> 198 bytes docs/img/@eaDir/events.png@SynoEAStream | Bin 0 -> 198 bytes docs/img/@eaDir/help_faq.png@SynoEAStream | Bin 0 -> 198 bytes docs/img/@eaDir/maintenance.png@SynoEAStream | Bin 0 -> 198 bytes docs/img/@eaDir/network.png@SynoEAStream | Bin 0 -> 198 bytes docs/img/@eaDir/presence.png@SynoEAStream | Bin 0 -> 198 bytes docs/img/@eaDir/settings.png@SynoEAStream | Bin 0 -> 198 bytes mkdocs.yml | 2 +- 14 files changed, 15 insertions(+), 131 deletions(-) delete mode 100755 docs/INSTALLATION_DOCKER.md create mode 100755 docs/img/@eaDir/device_details.png@SynoEAStream create mode 100755 docs/img/@eaDir/devices_dark.png@SynoEAStream create mode 100755 docs/img/@eaDir/devices_light.png@SynoEAStream create mode 100755 docs/img/@eaDir/devices_split.png@SynoEAStream create mode 100755 docs/img/@eaDir/events.png@SynoEAStream create mode 100755 docs/img/@eaDir/help_faq.png@SynoEAStream create mode 100755 docs/img/@eaDir/maintenance.png@SynoEAStream create mode 100755 docs/img/@eaDir/network.png@SynoEAStream create mode 100755 docs/img/@eaDir/presence.png@SynoEAStream create mode 100755 docs/img/@eaDir/settings.png@SynoEAStream diff --git a/.gitignore b/.gitignore index e9450f3a..70efbf39 100755 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,12 @@ .gitconfig .*CommandMarker deviceid -.VERSION - -# Sensitive data +.DS_Store .cache nohup.out config/* .ash_history +.VERSION config/pialert.conf config/app.conf db/* @@ -24,35 +23,23 @@ front/api/* /api/* **/plugins/**/*.log **/plugins/cloud_services/* +**/%40eaDir/ +**/@eaDir/ + +__pycache__/ +*.py[cod] +*$py.class + **/last_result.log **/script.log **/pialert.conf_bak **/pialert.db_bak +.*.swp -# future stuff front/img/cloud_services/* **/cloud_services.php **/cloud_services.js front/css/cloud_services.css -# OS junk -Thumbs.db -ehthumbs.db -Desktop.ini -**/%40eaDir/ -**/@eaDir/ -*/%40eaDir/ -*/@eaDir/ -.DS_Store -.*.swp - -# Python junk -venv/ -.env/ -__pycache__/ -*.py[cod] -*$py.class - -# Build artifacts (if you ever package anything) -dist/ -build/ \ No newline at end of file +docker-compose.yml.ffsb42 +.env.omada.ffsb42 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 9da575a2..8cb13f86 100755 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -4,10 +4,10 @@ NetAlertX can be installed several ways. The best supported option is Docker, followed by a supervised Home Assistant instance, as an Unraid app, and lastly, on bare metal. -- [[Installation] Docker (recommended)](./INSTALLATION_DOCKER.md) +- [[Installation] Docker (recommended)](https://github.com/jokob-sk/NetAlertX/blob/main/dockerfiles/README.md) - [[Installation] Home Assistant](https://github.com/alexbelgium/hassio-addons/tree/master/netalertx) - [[Installation] Unraid App](https://unraid.net/community/apps) -- [[Installation] Bare metal (experimental - looking for maintainers)](./HW_INSTALL.md) +- [[Installation] Bare metal (experimental - looking for maintainers)](https://github.com/jokob-sk/NetAlertX/blob/main/docs/HW_INSTALL.md) ## Help diff --git a/docs/INSTALLATION_DOCKER.md b/docs/INSTALLATION_DOCKER.md deleted file mode 100755 index 85cd1ecc..00000000 --- a/docs/INSTALLATION_DOCKER.md +++ /dev/null @@ -1,103 +0,0 @@ -[![Docker Size](https://img.shields.io/docker/image-size/jokobsk/netalertx?label=Size&logo=Docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx) -[![Docker Pulls](https://img.shields.io/docker/pulls/jokobsk/netalertx?label=Pulls&logo=docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx) -[![GitHub Release](https://img.shields.io/github/v/release/jokob-sk/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&style=for-the-badge)](https://github.com/jokob-sk/NetAlertX/releases) -[![Discord](https://img.shields.io/discord/1274490466481602755?color=0aa8d2&logoColor=fff&logo=Discord&style=for-the-badge)](https://discord.gg/NczTUTWyRr) -[![Home Assistant](https://img.shields.io/badge/Repo-blue?logo=home-assistant&style=for-the-badge&color=0aa8d2&logoColor=fff&label=Add)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons) - -# NetAlertX - Network scanner & notification framework - -| [📑 Docker guide](https://github.com/jokob-sk/NetAlertX/blob/main/dockerfiles/README.md) | [🚀 Releases](https://github.com/jokob-sk/NetAlertX/releases) | [📚 Docs](https://jokob-sk.github.io/NetAlertX/) | [🔌 Plugins](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md) | [🤖 Ask AI](https://gurubase.io/g/netalertx) -|----------------------| ----------------------| ----------------------| ----------------------| ----------------------| - - - - - -Head to [https://netalertx.com/](https://netalertx.com/) for more gifs and screenshots 📷. - -> [!NOTE] -> There is also an experimental 🧪 [bare-metal install](https://github.com/jokob-sk/NetAlertX/blob/main/docs/HW_INSTALL.md) method available. - -## 📕 Basic Usage - -> [!WARNING] -> You will have to run the container on the `host` network and specify `SCAN_SUBNETS` unless you use other [plugin scanners](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md). The initial scan can take a few minutes, so please wait 5-10 minutes for the initial discovery to finish. - -```yaml -docker run -d --rm --network=host \ - -v local_path/config:/app/config \ - -v local_path/db:/app/db \ - --mount type=tmpfs,target=/app/api \ - -e PUID=200 -e PGID=300 \ - -e TZ=Europe/Berlin \ - -e PORT=20211 \ - ghcr.io/jokob-sk/netalertx:latest -``` - -See alternative [docked-compose examples](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md). - -### Docker environment variables - -| Variable | Description | Example Value | -| :------------- |:------------------------| -----:| -| `PORT` |Port of the web interface | `20211` | -| `PUID` |Application User UID | `102` | -| `PGID` |Application User GID | `82` | -| `LISTEN_ADDR` |Set the specific IP Address for the listener address for the nginx webserver (web interface). This could be useful when using multiple subnets to hide the web interface from all untrusted networks. | `0.0.0.0` | -|`TZ` |Time zone to display stats correctly. Find your time zone [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | `Europe/Berlin` | -|`LOADED_PLUGINS` | Default [plugins](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md) to load. Plugins cannot be loaded with `APP_CONF_OVERRIDE`, you need to use this variable instead and then specify the plugins settings with `APP_CONF_OVERRIDE`. | `["PIHOLE","ASUSWRT"]` | -|`APP_CONF_OVERRIDE` | JSON override for settings (except `LOADED_PLUGINS`). | `{"SCAN_SUBNETS":"['192.168.1.0/24 --interface=eth1']","GRAPHQL_PORT":"20212"}` | -|`ALWAYS_FRESH_INSTALL` | ⚠ If `true` will delete the content of the `/db` & `/config` folders. For testing purposes. Can be coupled with [watchtower](https://github.com/containrrr/watchtower) to have an always freshly installed `netalertx`/`netalertx-dev` image. | `true` | - -> You can override the default GraphQL port setting `GRAPHQL_PORT` (set to `20212`) by using the `APP_CONF_OVERRIDE` env variable. `LOADED_PLUGINS` and settings in `APP_CONF_OVERRIDE` can be specified via the UI as well. - -### Docker paths - -> [!NOTE] -> See also [Backup strategies](https://github.com/jokob-sk/NetAlertX/blob/main/docs/BACKUPS.md). - -| Required | Path | Description | -| :------------- | :------------- | :-------------| -| ✅ | `:/app/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 | -| ✅ | `:/app/db` | Folder which will contain the `app.db` database file | -| | `:/app/log` | Logs folder useful for debugging if you have issues setting up the container | -| | `:/app/api` | A simple [API endpoint](https://github.com/jokob-sk/NetAlertX/blob/main/docs/API.md) containing static (but regularly updated) json and other files. | -| | `:/app/front/plugins//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. - -### 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 `/app/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. - -If you are running PiHole you can synchronize devices directly. Check the [PiHole configuration guide](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PIHOLE_GUIDE.md) for details. - -> [!NOTE] -> You can bulk-import devices via the [CSV import method](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEVICES_BULK_EDITING.md). - -#### Community guides - -You can read or watch several [community configuration guides](https://github.com/jokob-sk/NetAlertX/blob/main/docs/COMMUNITY_GUIDES.md) in Chinese, Korean, German, or French. - -> Please note these might be outdated. Rely on official documentation first. - -#### Common issues - -- Before creating a new issue, please check if a similar issue was [already resolved](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed). -- Check also common issues and [debugging tips](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEBUG_TIPS.md). - -## 💙 Support me - -| [![GitHub](https://i.imgur.com/emsRCPh.png)](https://github.com/sponsors/jokob-sk) | [![Buy Me A Coffee](https://i.imgur.com/pIM6YXL.png)](https://www.buymeacoffee.com/jokobsk) | [![Patreon](https://i.imgur.com/MuYsrq1.png)](https://www.patreon.com/user?u=84385063) | -| --- | --- | --- | - -- Bitcoin: `1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM` -- Ethereum: `0x6e2749Cb42F4411bc98501406BdcD82244e3f9C7` - -> 📧 Email me at [netalertx@gmail.com](mailto:netalertx@gmail.com?subject=NetAlertX Donations) if you want to get in touch or if I should add other sponsorship platforms. diff --git a/docs/img/@eaDir/device_details.png@SynoEAStream b/docs/img/@eaDir/device_details.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/devices_dark.png@SynoEAStream b/docs/img/@eaDir/devices_dark.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/devices_light.png@SynoEAStream b/docs/img/@eaDir/devices_light.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/devices_split.png@SynoEAStream b/docs/img/@eaDir/devices_split.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/events.png@SynoEAStream b/docs/img/@eaDir/events.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/help_faq.png@SynoEAStream b/docs/img/@eaDir/help_faq.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/maintenance.png@SynoEAStream b/docs/img/@eaDir/maintenance.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/network.png@SynoEAStream b/docs/img/@eaDir/network.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/presence.png@SynoEAStream b/docs/img/@eaDir/presence.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/docs/img/@eaDir/settings.png@SynoEAStream b/docs/img/@eaDir/settings.png@SynoEAStream new file mode 100755 index 0000000000000000000000000000000000000000..285322382cdb7510ee0de705bcd906c141b63be6 GIT binary patch literal 198 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@f08i@s9y95x_AdBnYYuqywZIWC}81 z1ahF_0#W&Ssd}C%sd*)tX_=`-3=GjAk&vRqyyCRfqF7!o5Z5!s)|i*eBfq#Lv?#|m UF)6>a#40ndB(*3nwS<=o0QWQ>*8l(j literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index e9936c6d..7bf864e6 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ nav: - Installation options: INSTALLATION.md - Quick setup: INITIAL_SETUP.md - Docker: - - Docker Guide: INSTALLATION_DOCKER.md + - Docker Guide: https://github.com/jokob-sk/NetAlertX/blob/main/dockerfiles/README.md - Docker Compose: DOCKER_COMPOSE.md - Docker File Permissions: FILE_PERMISSIONS.md - Docker Updates: UPDATES.md From b86f636b12d8404087ed03b9f19db0dcbf05717a Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 21:46:59 +1100 Subject: [PATCH 26/28] Revert "DOCS: clearer local_path instructions" This reverts commit dfc64fd85fdf600a9ed3485c90bd59f7255b7924. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2dea362..dec38950 100755 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Get visibility of what's going on on your WIFI/LAN network and enable presence d ## 🚀 Quick Start -To launch NetAlertX in seconds, replace `local_path` with the absolute path on your system that contains your `config` and `db` folders, then run:: +Start NetAlertX in seconds with Docker: ```bash docker run -d --rm --network=host \ From daea3a2cd7c406039f6035756521116a4bb5f39a Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 21:55:15 +1100 Subject: [PATCH 27/28] DOCS: WARNING use dockerhub docs Signed-off-by: jokob-sk --- docs/DOCKER_COMPOSE.md | 5 +++++ docs/DOCKER_MAINTENANCE.md | 5 +++++ docs/MIGRATION.md | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/docs/DOCKER_COMPOSE.md b/docs/DOCKER_COMPOSE.md index aafe96b9..abbe00d8 100755 --- a/docs/DOCKER_COMPOSE.md +++ b/docs/DOCKER_COMPOSE.md @@ -1,5 +1,10 @@ # 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. + 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] diff --git a/docs/DOCKER_MAINTENANCE.md b/docs/DOCKER_MAINTENANCE.md index f696c0eb..a538fa0a 100644 --- a/docs/DOCKER_MAINTENANCE.md +++ b/docs/DOCKER_MAINTENANCE.md @@ -1,5 +1,10 @@ # 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. + 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. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 84aa55cc..26b04d7f 100755 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,5 +1,11 @@ # 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] From ff96d38339141e6a149a0676a74f4fce73b62efa Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 31 Oct 2025 22:09:43 +1100 Subject: [PATCH 28/28] DOCS:old docker installation guide Signed-off-by: jokob-sk --- docs/DOCKER_INSTALLATION.md | 103 ++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 docs/DOCKER_INSTALLATION.md diff --git a/docs/DOCKER_INSTALLATION.md b/docs/DOCKER_INSTALLATION.md new file mode 100644 index 00000000..b1fe9cbd --- /dev/null +++ b/docs/DOCKER_INSTALLATION.md @@ -0,0 +1,103 @@ +[![Docker Size](https://img.shields.io/docker/image-size/jokobsk/netalertx?label=Size&logo=Docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx) +[![Docker Pulls](https://img.shields.io/docker/pulls/jokobsk/netalertx?label=Pulls&logo=docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx) +[![GitHub Release](https://img.shields.io/github/v/release/jokob-sk/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&style=for-the-badge)](https://github.com/jokob-sk/NetAlertX/releases) +[![Discord](https://img.shields.io/discord/1274490466481602755?color=0aa8d2&logoColor=fff&logo=Discord&style=for-the-badge)](https://discord.gg/NczTUTWyRr) +[![Home Assistant](https://img.shields.io/badge/Repo-blue?logo=home-assistant&style=for-the-badge&color=0aa8d2&logoColor=fff&label=Add)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons) + +# NetAlertX - Network scanner & notification framework + +| [📑 Docker guide](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_INSTALLATION.md) | [🚀 Releases](https://github.com/jokob-sk/NetAlertX/releases) | [📚 Docs](https://jokob-sk.github.io/NetAlertX/) | [🔌 Plugins](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md) | [🤖 Ask AI](https://gurubase.io/g/netalertx) +|----------------------| ----------------------| ----------------------| ----------------------| ----------------------| + + + + + +Head to [https://netalertx.com/](https://netalertx.com/) for more gifs and screenshots 📷. + +> [!NOTE] +> There is also an experimental 🧪 [bare-metal install](https://github.com/jokob-sk/NetAlertX/blob/main/docs/HW_INSTALL.md) method available. + +## 📕 Basic Usage + +> [!WARNING] +> You will have to run the container on the `host` network and specify `SCAN_SUBNETS` unless you use other [plugin scanners](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md). The initial scan can take a few minutes, so please wait 5-10 minutes for the initial discovery to finish. + +```yaml +docker run -d --rm --network=host \ + -v local_path/config:/app/config \ + -v local_path/db:/app/db \ + --mount type=tmpfs,target=/app/api \ + -e PUID=200 -e PGID=300 \ + -e TZ=Europe/Berlin \ + -e PORT=20211 \ + ghcr.io/jokob-sk/netalertx:latest +``` + +See alternative [docked-compose examples](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md). + +### Docker environment variables + +| Variable | Description | Example Value | +| :------------- |:------------------------| -----:| +| `PORT` |Port of the web interface | `20211` | +| `PUID` |Application User UID | `102` | +| `PGID` |Application User GID | `82` | +| `LISTEN_ADDR` |Set the specific IP Address for the listener address for the nginx webserver (web interface). This could be useful when using multiple subnets to hide the web interface from all untrusted networks. | `0.0.0.0` | +|`TZ` |Time zone to display stats correctly. Find your time zone [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | `Europe/Berlin` | +|`LOADED_PLUGINS` | Default [plugins](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS.md) to load. Plugins cannot be loaded with `APP_CONF_OVERRIDE`, you need to use this variable instead and then specify the plugins settings with `APP_CONF_OVERRIDE`. | `["PIHOLE","ASUSWRT"]` | +|`APP_CONF_OVERRIDE` | JSON override for settings (except `LOADED_PLUGINS`). | `{"SCAN_SUBNETS":"['192.168.1.0/24 --interface=eth1']","GRAPHQL_PORT":"20212"}` | +|`ALWAYS_FRESH_INSTALL` | ⚠ If `true` will delete the content of the `/db` & `/config` folders. For testing purposes. Can be coupled with [watchtower](https://github.com/containrrr/watchtower) to have an always freshly installed `netalertx`/`netalertx-dev` image. | `true` | + +> You can override the default GraphQL port setting `GRAPHQL_PORT` (set to `20212`) by using the `APP_CONF_OVERRIDE` env variable. `LOADED_PLUGINS` and settings in `APP_CONF_OVERRIDE` can be specified via the UI as well. + +### Docker paths + +> [!NOTE] +> See also [Backup strategies](https://github.com/jokob-sk/NetAlertX/blob/main/docs/BACKUPS.md). + +| Required | Path | Description | +| :------------- | :------------- | :-------------| +| ✅ | `:/app/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 | +| ✅ | `:/app/db` | Folder which will contain the `app.db` database file | +| | `:/app/log` | Logs folder useful for debugging if you have issues setting up the container | +| | `:/app/api` | A simple [API endpoint](https://github.com/jokob-sk/NetAlertX/blob/main/docs/API.md) containing static (but regularly updated) json and other files. | +| | `:/app/front/plugins//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. + +### 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 `/app/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. + +If you are running PiHole you can synchronize devices directly. Check the [PiHole configuration guide](https://github.com/jokob-sk/NetAlertX/blob/main/docs/PIHOLE_GUIDE.md) for details. + +> [!NOTE] +> You can bulk-import devices via the [CSV import method](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEVICES_BULK_EDITING.md). + +#### Community guides + +You can read or watch several [community configuration guides](https://github.com/jokob-sk/NetAlertX/blob/main/docs/COMMUNITY_GUIDES.md) in Chinese, Korean, German, or French. + +> Please note these might be outdated. Rely on official documentation first. + +#### Common issues + +- Before creating a new issue, please check if a similar issue was [already resolved](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed). +- Check also common issues and [debugging tips](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEBUG_TIPS.md). + +## 💙 Support me + +| [![GitHub](https://i.imgur.com/emsRCPh.png)](https://github.com/sponsors/jokob-sk) | [![Buy Me A Coffee](https://i.imgur.com/pIM6YXL.png)](https://www.buymeacoffee.com/jokobsk) | [![Patreon](https://i.imgur.com/MuYsrq1.png)](https://www.patreon.com/user?u=84385063) | +| --- | --- | --- | + +- Bitcoin: `1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM` +- Ethereum: `0x6e2749Cb42F4411bc98501406BdcD82244e3f9C7` + +> 📧 Email me at [netalertx@gmail.com](mailto:netalertx@gmail.com?subject=NetAlertX Donations) if you want to get in touch or if I should add other sponsorship platforms. diff --git a/mkdocs.yml b/mkdocs.yml index 7bf864e6..e2cb4dc7 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ nav: - Installation options: INSTALLATION.md - Quick setup: INITIAL_SETUP.md - Docker: - - Docker Guide: https://github.com/jokob-sk/NetAlertX/blob/main/dockerfiles/README.md + - Docker Guide: DOCKER_INSTALLATION.md - Docker Compose: DOCKER_COMPOSE.md - Docker File Permissions: FILE_PERMISSIONS.md - Docker Updates: UPDATES.md