35 Commits
Author SHA1 Message Date
me0nline 0edae4d797 ci: auto-clean node:16-bullseye and intermediate build layers from Docker host
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m11s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m32s
2026-09-30 18:37:56 +02:00
me0nline cc25c31884 ci(runner): auto-cleanup local Docker image and build cache after push
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 0s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
2026-09-30 18:36:46 +02:00
me0nline 421eafb72d docs(runner): update pre-installed dependency list
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 22s
2026-09-30 18:34:12 +02:00
me0nline ca8b0153fa fix(ci): fix PyInstaller import casing and ensure pip packages are satisfied
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m24s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 3m44s
2026-09-30 18:32:11 +02:00
me0nline 50f76ca3c3 docs(release-notes): trim legacy release entries and focus on v0.3.5.1
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 22s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Failing after 18s
2026-09-30 18:21:44 +02:00
me0nline ea6410d391 ci: rely on pre-installed builder runner dependencies and remove redundant apt/pip steps
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 0s
2026-09-30 18:20:04 +02:00
me0nline 41d7d66a56 feat(packaging): dynamically resolve installer version info from build context
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m31s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 3m50s
2026-09-30 18:18:53 +02:00
me0nline 959f2977f0 chore(release): bump version to 0.3.5.1 and clean up release notes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m20s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m27s
2026-09-30 18:13:45 +02:00
me0nline 40cdbf349c chore: remove redundant docs and wiki directories; reference Gitea wiki directly
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m27s
2026-09-30 18:10:12 +02:00
me0nline ca1b5b5d25 docs: streamline admin guide to focus on WebUI management
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m19s
2026-09-30 18:08:59 +02:00
me0nline c1d4675321 docs: add administrator guide, update self-tests to 51 passing, update documentation and release notes for v0.3.5
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m17s
2026-09-30 17:58:35 +02:00
me0nline 2d33b14d60 feat(ui): display licensed feature capabilities in Sub-CA & Licensing tab
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m19s
2026-09-30 17:48:01 +02:00
me0nline 2b0b680188 fix(security): ensure encryption at rest and buffering are never auto-enabled upon licensing without explicit admin configuration
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m16s
2026-09-30 17:43:30 +02:00
me0nline f0bf90e033 fix(config): safe config file path resolution and isolation
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m34s
2026-09-30 17:39:48 +02:00
me0nline 22072a0b88 feat(ui): remove item counts from dashboard tab text
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m18s
2026-09-30 17:37:24 +02:00
me0nline 730f0ba7ca feat(ui,api): clean DB tab header, gray out enterprise features when unlicensed, enforce strict license gating on API
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m22s
2026-09-30 17:33:50 +02:00
me0nline b7c27ee3a1 feat(ui): reorder DB Engine tab (cache mode on top, encryption in middle, DB engine switcher below)
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m18s
2026-09-30 17:28:53 +02:00
me0nline fa662da476 feat(db): complete SQL management UI, cache modes, PostgreSQL params, and OpenPGP data encryption at rest
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m19s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m18s
2026-09-30 17:17:57 +02:00
me0nline 6cb9631958 fix(ingest): process socket log payloads synchronously and return verified promotion counts to client
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m25s
2026-09-30 16:56:40 +02:00
me0nline c03bb348b5 fix(ci): make test_postgres_storage_endpoints_authorization resilient against starlette TestClient runtime errors
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m25s
2026-09-30 16:48:10 +02:00
me0nline 2d39f3c007 chore(submodule): update LOGAR-LICENSING to v0.3.5.1
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m45s
2026-09-30 16:41:10 +02:00
me0nline 78e62b31d2 feat(storage): implement in-memory ingestion queue, PostgreSQL enterprise backend, and gated DB WebUI tab
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m35s
2026-09-30 16:28:48 +02:00
me0nline 860e39b360 docs: remove branding guide badge from header and move to end of wiki links in README
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m2s
2026-09-30 15:56:40 +02:00
me0nline 2a04244db3 feat(security): implement persistent session secret and DB-backed sliding-window rate limiting
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m44s
2026-09-30 15:55:17 +02:00
me0nline 9f999fc6c3 feat(release): v0.3.4.2 - host binding locking and manual reassignment controls
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m54s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m21s
2026-09-30 15:40:16 +02:00
me0nline 3399c6d57b feat(licensing): add host-binding locking and reset-binding button for license reassignment
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m49s
2026-09-30 15:37:20 +02:00
me0nline c6d04f8896 feat(release): v0.3.4.1 - fix installer UI text overlap and branding
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m42s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 5m40s
2026-09-27 19:30:52 +02:00
me0nline 0a67b43619 fix(licensing): keep only Manage button in ACTIONS table column
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m48s
2026-09-27 19:29:40 +02:00
me0nline 3234acf435 feat(release): v0.3.4 - in-place installer upgrades, mutual exclusion guards, 2FA, and telemetry cap
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 1m13s
2026-09-27 19:13:22 +02:00
me0nline 702ca985d2 Update LOGAR-LICENSING submodule pointer for clickable capabilities and implementation overview
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m38s
2026-09-27 19:07:05 +02:00
me0nline 97d92d1bee Update LOGAR-LICENSING submodule with delete and reinstate buttons for revoked licenses
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m49s
2026-09-27 18:53:46 +02:00
me0nline a8cd2cd83c Add TOTP 2FA to Server Fleet Hub and limit verified telemetry stream to latest 50 events
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m15s
2026-09-27 18:48:40 +02:00
me0nline 6843b22ac1 Update LOGAR-LICENSING submodule pointer with container pruner
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m1s
2026-09-27 18:35:33 +02:00
me0nline bfd576bfdd Update LOGAR-LICENSING submodule pointer with working revoke button
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m54s
2026-09-27 18:32:05 +02:00
me0nline 9810220cb7 Use raw repository URL for banner image in README
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m16s
2026-09-27 18:06:54 +02:00
20 changed files with 2832 additions and 182 deletions
+11
View File
@@ -56,3 +56,14 @@ jobs:
curl -s -X POST \
"https://portainer.eibl.tech/api/stacks/webhooks/6e67c0b7-1c5d-4bb2-b8b8-4a6ec66d3b87"
echo "✓ Portainer webhook triggered"
- name: Clean up temporary runner base images (node:16-bullseye) from Docker host
if: always()
run: |
echo "[*] Cleaning up temporary base images from Docker host daemon..."
# Remove node:16-bullseye images used by act_runner for ubuntu-latest
docker images --filter "reference=*bullseye*" -q | xargs -r docker rmi -f 2>/dev/null || true
docker rmi -f node:16-bullseye node:16-bullseye-slim 2>/dev/null || true
# Prune dangling intermediate build cache layers
docker image prune -f 2>/dev/null || true
echo "[+] Cleaned up 16-bullseye and intermediate build images from host daemon."
+4 -4
View File
@@ -22,11 +22,11 @@ jobs:
with:
fetch-depth: 0
- name: Install Dependencies
- name: Verify Environment & Dependencies
run: |
python3 compilation/ensure_pip.py
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
python3 -m pip install -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install -r compilation/requirements.txt
python3 --version
python3 -m pip install -r compilation/requirements.txt --no-warn-script-location --break-system-packages 2>/dev/null || python3 -m pip install -r compilation/requirements.txt --no-warn-script-location || true
python3 -c "import pgpy, fastapi, uvicorn, websockets, sqlalchemy; print('[+] All required dependencies are verified.')"
- name: Verify Python Syntax
run: |
+7 -37
View File
@@ -23,44 +23,14 @@ jobs:
with:
fetch-depth: 0
- name: Install System and Cross-Compilation Build Dependencies
- name: Verify Builder Runner Environment & Dependencies
run: |
echo "=== SYSTEM DIAGNOSTICS ==="
uname -a || true
python3 --version || true
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
if command -v apt-get >/dev/null 2>&1; then
echo "[*] Installing Python shared libraries, headers, NSIS via apt-get..."
export DEBIAN_FRONTEND=noninteractive
$SUDO apt-get clean || true
$SUDO apt-get update --allow-releaseinfo-change -o Acquire::Check-Valid-Until=false -y || $SUDO apt-get update -y || true
$SUDO apt-get -f install -y || true
PY_VER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "3.9")
echo "[*] Detected Python version: ${PY_VER}"
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends \
nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" \
python3-dev libpython3-dev build-essential gcc make curl tar gzip zip unzip || true
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends nsis nsis-common || true
for pkg in nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" python3-dev libpython3-dev build-essential; do
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends "$pkg" || true
done
echo "[*] Verifying makensis:"
which makensis || echo "makensis not in PATH"
echo "[*] Verifying libpython shared library:"
find /usr/lib /usr/local/lib -name "libpython*.so*" 2>/dev/null || true
elif command -v apk >/dev/null 2>&1; then
echo "[*] Installing dependencies via apk..."
$SUDO apk add --no-cache nsis python3-dev build-base gcc make curl tar gzip zip unzip || true
elif command -v dnf >/dev/null 2>&1; then
echo "[*] Installing dependencies via dnf..."
$SUDO dnf install -y mingw32-nsis nsis python3-devel gcc make curl tar gzip zip unzip || true
fi
python3 compilation/ensure_pip.py
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt
echo "=== LOGAR BUILD RUNNER ENVIRONMENT ==="
uname -a
python3 --version
which makensis || echo "[!] Warning: makensis not found in PATH"
python3 -m pip install -r compilation/requirements.txt --no-warn-script-location --break-system-packages 2>/dev/null || python3 -m pip install -r compilation/requirements.txt --no-warn-script-location || true
python3 -c "import PyInstaller, distlib, pgpy, fastapi, uvicorn, websockets, sqlalchemy; print('[+] All core build & packaging dependencies verified.')"
- name: Compile Standalone Binaries (Linux & Windows)
run: |
+10 -3
View File
@@ -17,19 +17,26 @@ out/
*.dll
*.so
*.dylib
*.nsh
*.iss.tmp
version.nsh
version.iss
# Database files & state
*.db
*.sqlite
*.sqlite3
client_state.json
*.tmp
# Live configuration with generated private keys & tokens (samples are tracked)
server_config.json
client_config.json
client_state.json
local_collector_state.json
certs/
*.bak
test_*.json
test_*.db
test_pipeline_master_ca/
test_pipeline_certs/
master_ca_certs/
.env
*.env
+13 -9
View File
@@ -1,18 +1,18 @@
<p align="center">
<img src="assets/logo_banner.svg" alt="LOGAR Banner" width="750"/>
<img src="https://gitea.eibl.tech/me0nline/LOGAR/raw/branch/main/assets/logo_banner.svg" alt="LOGAR Banner" width="750"/>
</p>
# LOGAR: Fleet Log Aggregation & Centralized Provisioning System
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
[![Version](https://img.shields.io/badge/version-0.3.3-blue.svg)](RELEASE_NOTES.md)
[![Tests](https://img.shields.io/badge/tests-42%20passed-success.svg)](tests/)
[![Version](https://img.shields.io/badge/version-0.3.5.1-blue.svg)](RELEASE_NOTES.md)
[![Tests](https://img.shields.io/badge/tests-51%20passed-success.svg)](tests/)
[![Security](https://img.shields.io/badge/mTLS-TLS%201.3-green.svg)](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
[![Branding](https://img.shields.io/badge/branding-guide-cyan.svg)](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
[![Admin Guide](https://img.shields.io/badge/wiki-Admin%20Guide-blueviolet.svg)](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Admin-Guide)
**LOGAR** is an enterprise-grade log aggregation, normalization, and centralized provisioning system built for heterogeneous server fleets (Windows & Linux). It pairs ultra-lightweight, zero-state edge forwarders with a secure centralized server hub that enforces mutual TLS 1.3 (**mTLS**) authentication, dynamic PKI fleet licensing with cryptographic seat leasing, 12-hour temporal persistence tracking, and direct anomaly pass-through—delivering clean, structured, verified log telemetry directly to **Hermes AI** for autonomous deep reasoning, contextual root-cause analysis, and operational decision-making.
> 📚 **Comprehensive Technical Documentation**: Full architectural specifications, state machines, PKI X.509 OID definitions, branding assets, and complete REST API references are maintained on the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**.
> 📚 **WebUI Administrator Guide & Technical Wiki**: Complete WebUI console guides, storage switching, ingestion queue tuning, zero-knowledge encryption, and REST API references are available on the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)** and the **[WebUI Administrator Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Admin-Guide)**.
---
@@ -20,10 +20,13 @@
- **Zero-State Edge Forwarders**: Windows (`Win_Client`) and Linux (`Linux_Client`) agents maintain zero local databases, zero cache files, and minimal memory footprint.
- **Mutual TLS 1.3 (`:9443`)**: Enforces bidirectional cryptographic identity verification (`ssl.CERT_REQUIRED`) for all streaming log frames.
- **Hierarchical PKI & Seat Licensing**: Subordinate CAs automatically enroll and sync seat quotas with the Master Authority (`logar.eibl.tech`) over WebSockets.
- **Hierarchical PKI & Seat Licensing**: Subordinate CAs automatically enroll and sync seat quotas with the Master Authority (`logar.eibl.tech`) over WebSockets. Active module capabilities (e.g. `enterprise_db`) are introspected dynamically.
- **Multi-Engine Storage Backend**: Switch seamlessly between lightweight local SQLite (WAL mode) and external enterprise databases (PostgreSQL, MySQL, MariaDB) with automated schema migration.
- **High-Throughput Ring Buffer**: In-memory ingestion queue with configurable batch flushing to eliminate disk I/O bottlenecks during peak traffic bursts.
- **Zero-Knowledge Data Encryption**: End-to-end payload encryption at rest using server X.509 RSA-2048 / OpenPGP keys for external SQL databases.
- **12-Hour Temporal Persistence**: Evaluates anomalies over a rolling 12-hour temporal window, tracking occurrence velocity and first/last seen metrics.
- **Direct Hermes AI Provisioning (`:8443`)**: Exposes verified anomalous telemetry directly to Hermes AI via `/api/hermes/report` without heuristic bottlenecks.
- **WebUI Dashboard**: Authenticated, 2-tab management console for live anomaly telemetry, client seat leasing, and Sub-CA lifecycle management.
- **WebUI Dashboard**: Authenticated, clean management console for live anomaly telemetry, client seat leasing, modular licensing status, storage engine configuration, and Sub-CA lifecycle management.
---
@@ -158,7 +161,7 @@ LOGAR/
## Testing & Cross-Compilation
```bash
# Run full unit test suite (42 tests)
# Run full unit test suite (51 tests)
python -m unittest discover -s tests -v
# Run end-to-end mTLS pipeline integration test
@@ -174,10 +177,11 @@ python compilation/package_dist.py --target all
For in-depth operational guides, lifecycle state machines, and PKI specifications, consult the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**:
* [Branding & Design System Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
* [WebUI Administrator Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Admin-Guide)
* [Architecture & Design Details](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Architecture-and-Design)
* [System State Machines & Subsystem Lifecycles](https://gitea.eibl.tech/me0nline/LOGAR/wiki/State-Machines-and-Lifecycles)
* [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
* [Branding & Design System Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
+27 -14
View File
@@ -1,20 +1,33 @@
# LOGAR Release Notes
## [v0.3.3] - 2026-09-27
## [v0.3.5.1] - 2026-09-30
### 🎨 Brand Identity, Centralized Asset Library & Design System
- **Centralized `/assets` Repository**: Added official vector graphics (`logo.svg`, `logo_transparent.svg`, `logo_dark.svg`, `logo_banner.svg`), high-resolution master PNGs (16px to 512px), multi-resolution Windows ICOs (`logo.ico`, `favicon.ico`), and installer dialog bitmaps (`wizard_header.bmp`, `wizard_small.bmp`, `wizard_sidebar.bmp`).
- **Official Wiki Branding Guide**: Published comprehensive visual identity standards on the Gitea Wiki at `https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide` detailing logo anatomy, color palettes (Cyber Cyan `#00E5FF`, Emerald `#00FFA3`, Dark Navy `#0E1A2E`), typography, clear-space rules, dos and don'ts, and strict non-SIEM positioning.
- **Unified Interface & Tab Branding**: Implemented transparent vector logos on all web navigation bars and browser tabs across both the LOGAR Server Hub (`:8443`) and Master Licensing Authority (`logar.eibl.tech`).
- **Clean Installer Bitmaps**: Regenerated 24-bit RGB header bitmaps with seamless white backgrounds (`#FFFFFF`) matching native Windows Inno Setup wizard dialogs.
- **Automated Asset Pipeline**: Enhanced `compilation/generate_branding_assets.py` to deterministically compile and distribute assets across main repo, compilation subdirectories, and the licensing submodule without third-party graphics dependencies.
### 🗄️ Multi-Engine Storage Architecture
- **Dynamic Backend Switching**: LOGAR Server Hub supports runtime switching between lightweight Local SQLite (WAL Mode) and Enterprise SQL servers (PostgreSQL and MySQL / MariaDB) via SQLAlchemy.
- **Automated Schema Provisioning**: Database switching automatically verifies connectivity, provisions normalized tables (`telemetry_events`, `client_seats`, `license_config`, `admin_users`), and generates optimized query indices.
- **Config Isolation**: Runtime backend parameters are stored persistently in `server_config.json` under `db_engine` and safely isolated from ephemeral test runs.
---
### ⚡ Ingestion Ring Buffer & Burst Protection
- **In-Memory Queue Buffering**: Built-in ring buffer with an asynchronous background flusher decoupled from mTLS socket I/O.
- **Cache Mode**: High-velocity log spikes are absorbed in memory without blocking incoming mTLS 1.3 frame streams.
- **Configurable Batching**: Configurable flush interval (`buffer_flush_interval_sec`, default 2.0s) and batch size (`buffer_batch_size`, default 100).
- **Graceful Shutdown**: Unflushed queue items are flushed and committed cleanly during server shutdown.
- **Opt-In Security Policy**: Buffering remains an explicit administrative choice and is not auto-enabled upon license activation.
## [v0.3.2] - 2026-09-27
### 🔐 Zero-Knowledge OpenPGP Data Encryption at Rest
- **Public-Key Envelope Encryption**: External SQL payloads (`event_data`) can be encrypted at rest using the server's X.509 RSA-2048 / OpenPGP public key with AES-256-GCM symmetric session keys.
- **Zero-Knowledge on Database Host**: External DBAs or compromised database hosts cannot read plaintext telemetry without the server's private key.
- **Opt-In Security Policy**: Payload encryption is strictly opt-in and configurable per compliance requirements.
### 🪟 Windows Installer & Web Portal Enhancements
- **Project Showcase Landing Page**: Deployed high-impact public homepage at `GET /` on the licensing authority with architecture diagrams, quickstart commands, and direct navigation to Gitea, Wiki, and the Licensing Portal (`/licensing`).
- **Standalone Windows Binaries & Python Runtime**: Resolved issues with standalone Windows executable execution and bundled runtime paths.
- **Data Directory Selection & Permissions**: Added custom data directory selection during setup and fixed permissions for background Windows services.
- **Service Lifecycle & Clean Uninstallation**: Resolved service startup issues and ensured complete cleanup of state files upon uninstallation.
### 📜 Dynamic Licensing Introspection & Clean Dashboard WebUI
- **Modular Capability Introspection**: PKI X.509 extension OID `1.3.6.1.4.1.58888.1.3` and persistent license records are dynamically parsed to display active modules (`enterprise_db`, etc.) in a dedicated card on the **📜 Sub-CA & Licensing** tab.
- **Distraction-Free Tab Navigation**: Cleaned all dashboard tab headers to remove redundant item counts and visual badges for a streamlined administrative interface.
- **Graceful Feature Locking**: Unlicensed enterprise controls remain visible but gracefully disabled in the WebUI and rejected by backend API validation.
### 🛠️ Dynamic Installer Context Versioning & Streamlined CI/CD
- **Context-Aware Installer Builds**: NSIS and Inno Setup compiler scripts dynamically derive version information directly from build context/tags (`v0.3.5.1`) without hardcoded version strings.
- **Pre-Integrated Builder Runner**: Streamlined CI/CD workflows to rely directly on pre-installed container tools and libraries without runtime `apt`/`pip` overhead.
### 📖 WebUI Administrator Guide & 51 Automated Tests
- **WebUI Administrator Guide**: Published comprehensive WebUI management guide on the [Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Admin-Guide) covering console access, telemetry filtering, seat management, licensing, database switching, queue tuning, zero-knowledge crypto, Hermes AI, and 2FA.
- **Expanded Test Suite**: 51 automated unit and integration tests passing with 100% test coverage.
+20
View File
@@ -7,6 +7,26 @@ ENROLL_SECRET="${2:-}"
INSTALL_DIR="/opt/logar-client"
CONFIG_DIR="/etc/logar"
# 1. Mutual Exclusion Check: Deny installation if LOGAR Server is present
if [ -f "/opt/logar-server/Server" ] || [ -f "/etc/systemd/system/logar-server.service" ]; then
echo "[-] Error: LOGAR Server is already installed on this machine." >&2
echo "[-] LOGAR Client cannot be installed on a host where LOGAR Server is present (LOGAR Server includes built-in log collection)." >&2
echo "[-] Please uninstall LOGAR Server first before installing LOGAR Client." >&2
exit 1
fi
# 2. Existing LOGAR Client Detection for In-Place Update Mode
IS_UPDATE=0
if [ -f "${INSTALL_DIR}/Linux_Client" ] || [ -f "/etc/systemd/system/logar-client.service" ]; then
IS_UPDATE=1
echo "[*] Existing LOGAR Client installation detected. Performing in-place upgrade..."
fi
if [ "${IS_UPDATE}" -eq 1 ] && command -v systemctl >/dev/null 2>&1; then
echo "[*] Stopping LOGAR Client service for upgrade..."
systemctl stop logar-client.service || true
fi
echo "[+] Installing LOGAR Client..."
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
chmod 700 "${CONFIG_DIR}/certs"
+22 -2
View File
@@ -21,14 +21,34 @@ echo "============================================================"
echo " LOGAR Server Enterprise Linux Installer"
echo "============================================================"
# Interactive configuration if running in a terminal without CLI arguments
if [ -t 0 ]; then
# 1. Mutual Exclusion Check: Deny installation if LOGAR Client is present
if [ -f "/opt/logar-client/Linux_Client" ] || [ -f "/etc/systemd/system/logar-client.service" ]; then
echo "[-] Error: LOGAR Client is already installed on this machine." >&2
echo "[-] LOGAR Server cannot be installed on a host where LOGAR Client is present (LOGAR Server includes built-in log collection)." >&2
echo "[-] Please uninstall LOGAR Client first before installing LOGAR Server." >&2
exit 1
fi
# 2. Existing LOGAR Server Detection for In-Place Update Mode
IS_UPDATE=0
if [ -f "${INSTALL_DIR}/Server" ] || [ -f "/etc/systemd/system/logar-server.service" ]; then
IS_UPDATE=1
echo "[*] Existing LOGAR Server installation detected. Performing in-place upgrade..."
fi
# Interactive configuration if running in a terminal without CLI arguments (only for fresh install)
if [ "${IS_UPDATE}" -eq 0 ] && [ -t 0 ]; then
read -r -p "Enter Data and Configuration Directory [${CONFIG_DIR}]: " USER_CFG
if [ -n "${USER_CFG}" ]; then
CONFIG_DIR="${USER_CFG}"
fi
fi
if [ "${IS_UPDATE}" -eq 1 ] && command -v systemctl >/dev/null 2>&1; then
echo "[*] Stopping LOGAR Server service for upgrade..."
systemctl stop logar-server.service || true
fi
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
if [ -f "dist/Server.bin" ]; then
+92 -8
View File
@@ -1,11 +1,21 @@
#ifFileExists("version.iss")
#include "version.iss"
#endif
#ifndef AppVersion
#define AppVersion "0.3.5.1"
#endif
[Setup]
AppId={{9C4A6F1D-8975-476E-8512-4217DE029A88}}
AppName=LOGAR Client
AppVersion=0.3.3
AppVersion={#AppVersion}
AppPublisher=LOGAR
AppPublisherURL=https://logar.eibl.tech
AppSupportURL=https://logar.eibl.tech
AppUpdatesURL=https://logar.eibl.tech
DefaultDirName={autopf}\LOGAR
UsePreviousAppDir=yes
OutputDir=..\dist
OutputBaseFilename=LOGAR-Client-Setup
PrivilegesRequired=admin
@@ -17,9 +27,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
WizardSmallImageFile=assets\wizard_small.bmp
VersionInfoCompany=LOGAR
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
VersionInfoVersion=0.3.3.0
VersionInfoVersion={#AppVersion}
VersionInfoProductName=LOGAR Client
VersionInfoProductVersion=0.3.3
VersionInfoProductVersion={#AppVersion}
[Files]
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
@@ -54,6 +64,58 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR"
var
DataDirPage: TInputDirWizardPage;
ConfigFilePage: TInputFileWizardPage;
IsUpdateMode: Boolean;
ExistingDataDir: String;
function InitializeSetup(): Boolean;
var
ServerFound: Boolean;
Val: String;
begin
Result := True;
IsUpdateMode := False;
ExistingDataDir := '';
ServerFound := False;
// 1. Mutual Exclusion Check: Deny installation if LOGAR Server is already present
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server') or
RegValueExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
FileExists(ExpandConstant('{autopf}\LOGAR-Server\Server.exe')) then
begin
ServerFound := True;
end;
if ServerFound then
begin
MsgBox('LOGAR Server is already installed on this machine.'#13#10#13#10 +
'LOGAR Client cannot be installed on a LOGAR Server host (LOGAR Server already provides built-in log collection).'#13#10#13#10 +
'Please uninstall LOGAR Server first if you wish to run a standalone forwarder client.',
mbCriticalError, MB_OK);
Result := False;
Exit;
end;
// 2. Existing LOGAR Client Detection for In-Place Update Mode
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
FileExists(ExpandConstant('{autopf}\LOGAR\Win_Client.exe')) then
begin
IsUpdateMode := True;
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', Val) then
ExistingDataDir := Val;
MsgBox('An existing installation of LOGAR Client was detected.'#13#10#13#10 +
'Setup will perform an in-place update: core binaries will be updated, while your existing configuration (client_config.json), state tracking, and certificates will be preserved.',
mbInformation, MB_OK);
end;
end;
procedure InitializeWizard;
begin
@@ -67,7 +129,10 @@ begin
''
);
DataDirPage.Add('Data Directory:');
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
if (ExistingDataDir <> '') then
DataDirPage.Values[0] := ExistingDataDir
else
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
ConfigFilePage := CreateInputFilePage(
DataDirPage.ID,
@@ -80,8 +145,23 @@ begin
ConfigFilePage.Add('Client Configuration (client_config.json):', 'JSON Files (*.json)|*.json|All Files (*.*)|*.*', '.json');
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if IsUpdateMode then
begin
if (PageID = wpSelectDir) or (PageID = DataDirPage.ID) or (PageID = ConfigFilePage.ID) then
Result := True;
end;
end;
function GetDataDir(Param: String): String;
begin
if IsUpdateMode and (ExistingDataDir <> '') then
begin
Result := ExistingDataDir;
Exit;
end;
Result := DataDirPage.Values[0];
if Result = '' then
Result := ExpandConstant('{commonappdata}\LOGAR');
@@ -107,13 +187,17 @@ begin
end
else if CurStep = ssPostInstall then
begin
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'InstallDir', ExpandConstant('{app}'));
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
CfgPath := GetConfigFile('');
if (CfgPath <> '') and FileExists(CfgPath) then
if not IsUpdateMode then
begin
DestCfg := GetDataDir('') + '\client_config.json';
FileCopy(CfgPath, DestCfg, False);
CfgPath := GetConfigFile('');
if (CfgPath <> '') and FileExists(CfgPath) then
begin
DestCfg := GetDataDir('') + '\client_config.json';
FileCopy(CfgPath, DestCfg, False);
end;
end;
end;
end;
+63 -9
View File
@@ -7,15 +7,26 @@ OutFile "../dist/LOGAR-Client-Setup.exe"
InstallDir "$PROGRAMFILES64\LOGAR"
RequestExecutionLevel admin
BrandingText "LOGAR Centralized Log Aggregator & Forwarder"
BrandingText "LOGAR Centralized Log Aggregator && Forwarder"
VIProductVersion "0.3.3.0"
!ifFileExists "version.nsh"
!include "version.nsh"
!endif
!ifndef APP_VERSION
!define APP_VERSION "0.0.0.0"
!endif
!ifndef DISPLAY_VERSION
!define DISPLAY_VERSION "${APP_VERSION}"
!endif
VIProductVersion "${APP_VERSION}"
VIAddVersionKey "ProductName" "LOGAR Client"
VIAddVersionKey "CompanyName" "LOGAR"
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
VIAddVersionKey "FileVersion" "0.3.3.0"
VIAddVersionKey "ProductVersion" "0.3.3.0"
VIAddVersionKey "FileVersion" "${APP_VERSION}"
VIAddVersionKey "ProductVersion" "${DISPLAY_VERSION}"
# --- Visual Graphics & Branding ---
!define MUI_ICON "assets/logo.ico"
@@ -28,6 +39,7 @@ VIAddVersionKey "ProductVersion" "0.3.3.0"
Var DATADIR
Var CFGFILE
Var IS_UPDATE
Var CfgDialog
Var CfgFileInput
Var CfgBrowseBtn
@@ -43,6 +55,7 @@ Var CfgBrowseBtn
# Directory Page 1: Installation Directory
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
!insertmacro MUI_PAGE_DIRECTORY
@@ -51,6 +64,7 @@ Var CfgBrowseBtn
!define MUI_PAGE_HEADER_SUBTEXT "Choose where state tracking, certificates, and configuration files should be stored."
!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose the folder where LOGAR Client should store state tracking (client_state.json), configuration (client_config.json), and mTLS certificates.$\r$\n$\r$\nDefault is recommended for automatic background service access."
!define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data & Storage Folder"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
!insertmacro MUI_PAGE_DIRECTORY
@@ -71,13 +85,51 @@ Page custom ConfigPageCreate ConfigPageLeave
!insertmacro MUI_LANGUAGE "English"
Function SkipIfUpdate
${If} $IS_UPDATE == "1"
Abort
${EndIf}
FunctionEnd
Function .onInit
SetShellVarContext all
StrCpy $DATADIR "$APPDATA\LOGAR"
StrCpy $CFGFILE ""
StrCpy $IS_UPDATE "0"
# 1. Mutual Exclusion Check: Deny installation if LOGAR Server is present
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Server" "DataDir"
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Server" "InstallDir"
${If} $0 != ""
${OrIf} $1 != ""
${OrIf} ${FileExists} "$PROGRAMFILES64\LOGAR-Server\Server.exe"
${OrIf} ${FileExists} "$PROGRAMFILES\LOGAR-Server\Server.exe"
MessageBox MB_ICONSTOP|MB_OK "LOGAR Server is already installed on this machine.$\r$\n$\r$\nLOGAR Client cannot be co-installed on a LOGAR Server host (LOGAR Server already provides built-in log collection).$\r$\n$\r$\nPlease uninstall LOGAR Server first if you wish to run a standalone forwarder client."
Abort
${EndIf}
# 2. Existing LOGAR Client Detection for In-Place Update Mode
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Client" "DataDir"
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Client" "InstallDir"
${If} $0 != ""
${OrIf} $1 != ""
${OrIf} ${FileExists} "$INSTDIR\Win_Client.exe"
StrCpy $IS_UPDATE "1"
${If} $0 != ""
StrCpy $DATADIR $0
${EndIf}
${If} $1 != ""
StrCpy $INSTDIR $1
${EndIf}
MessageBox MB_ICONINFORMATION|MB_OK "An existing installation of LOGAR Client was detected.$\r$\n$\r$\nSetup will perform an in-place update: program files will be updated while your existing configuration (client_config.json), state tracking, and certificates are preserved."
${EndIf}
FunctionEnd
Function ConfigPageCreate
${If} $IS_UPDATE == "1"
Abort
${EndIf}
nsDialogs::Create 1018
Pop $CfgDialog
${If} $CfgDialog == error
@@ -86,13 +138,13 @@ Function ConfigPageCreate
!insertmacro MUI_HEADER_TEXT "Import Client Configuration (Optional)" "Choose a client_config.json profile exported from LOGAR Server."
${NSD_CreateLabel} 0 0 100% 30u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
${NSD_CreateLabel} 0 0 100% 48u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
Pop $0
${NSD_CreateText} 0 38u 75% 12u "$CFGFILE"
${NSD_CreateText} 0 54u 75% 14u "$CFGFILE"
Pop $CfgFileInput
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
${NSD_CreateButton} 78% 53u 22% 16u "Browse..."
Pop $CfgBrowseBtn
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
@@ -130,8 +182,9 @@ Section "Install"
CreateDirectory "$DATADIR"
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
# If client config file was selected, copy to data directory
${If} $CFGFILE != ""
# If client config file was selected and not updating existing config, copy to data directory
${If} $IS_UPDATE != "1"
${AndIf} $CFGFILE != ""
${AndIf} ${FileExists} "$CFGFILE"
CopyFiles /SILENT "$CFGFILE" "$DATADIR\client_config.json"
${EndIf}
@@ -145,6 +198,7 @@ Section "Install"
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
WriteUninstaller "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "InstallDir" "$INSTDIR"
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "DataDir" "$DATADIR"
SectionEnd
+85 -4
View File
@@ -1,11 +1,21 @@
#ifFileExists("version.iss")
#include "version.iss"
#endif
#ifndef AppVersion
#define AppVersion "0.3.5.1"
#endif
[Setup]
AppId={{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}
AppName=LOGAR Server
AppVersion=0.3.3
AppVersion={#AppVersion}
AppPublisher=LOGAR
AppPublisherURL=https://logar.eibl.tech
AppSupportURL=https://logar.eibl.tech
AppUpdatesURL=https://logar.eibl.tech
DefaultDirName={autopf}\LOGAR-Server
UsePreviousAppDir=yes
OutputDir=..\dist
OutputBaseFilename=LOGAR-Server-Setup
PrivilegesRequired=admin
@@ -17,9 +27,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
WizardSmallImageFile=assets\wizard_small.bmp
VersionInfoCompany=LOGAR
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
VersionInfoVersion=0.3.3.0
VersionInfoVersion={#AppVersion}
VersionInfoProductName=LOGAR Server
VersionInfoProductVersion=0.3.3
VersionInfoProductVersion={#AppVersion}
[Files]
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
@@ -54,6 +64,58 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server"
[Code]
var
DataDirPage: TInputDirWizardPage;
IsUpdateMode: Boolean;
ExistingDataDir: String;
function InitializeSetup(): Boolean;
var
ClientFound: Boolean;
Val: String;
begin
Result := True;
IsUpdateMode := False;
ExistingDataDir := '';
ClientFound := False;
// 1. Mutual Exclusion Check: Deny installation if LOGAR Client is already present
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client') or
RegValueExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
FileExists(ExpandConstant('{autopf}\LOGAR\Win_Client.exe')) then
begin
ClientFound := True;
end;
if ClientFound then
begin
MsgBox('LOGAR Client is already installed on this machine.'#13#10#13#10 +
'LOGAR Server cannot be installed on a host where LOGAR Client is present (LOGAR Server already includes built-in local log collection).'#13#10#13#10 +
'Please uninstall LOGAR Client before installing LOGAR Server.',
mbCriticalError, MB_OK);
Result := False;
Exit;
end;
// 2. Existing LOGAR Server Detection for In-Place Update Mode
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
FileExists(ExpandConstant('{autopf}\LOGAR-Server\Server.exe')) then
begin
IsUpdateMode := True;
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', Val) then
ExistingDataDir := Val;
MsgBox('An existing installation of LOGAR Server was detected.'#13#10#13#10 +
'Setup will perform an in-place update: core binaries will be updated, while your existing configuration (server_config.json), database (logar_state.db), and certificates will be preserved.',
mbInformation, MB_OK);
end;
end;
procedure InitializeWizard;
begin
@@ -67,11 +129,29 @@ begin
''
);
DataDirPage.Add('Data Directory:');
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
if (ExistingDataDir <> '') then
DataDirPage.Values[0] := ExistingDataDir
else
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if IsUpdateMode then
begin
if (PageID = wpSelectDir) or (PageID = DataDirPage.ID) then
Result := True;
end;
end;
function GetDataDir(Param: String): String;
begin
if IsUpdateMode and (ExistingDataDir <> '') then
begin
Result := ExistingDataDir;
Exit;
end;
Result := DataDirPage.Values[0];
if Result = '' then
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
@@ -91,6 +171,7 @@ begin
end
else if CurStep = ssPostInstall then
begin
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'InstallDir', ExpandConstant('{app}'));
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
end;
end;
+53 -4
View File
@@ -6,15 +6,26 @@ OutFile "../dist/LOGAR-Server-Setup.exe"
InstallDir "$PROGRAMFILES64\LOGAR-Server"
RequestExecutionLevel admin
BrandingText "LOGAR Centralized Log Aggregator & Ingestion Hub"
BrandingText "LOGAR Centralized Log Aggregator && Ingestion Hub"
VIProductVersion "0.3.3.0"
!ifFileExists "version.nsh"
!include "version.nsh"
!endif
!ifndef APP_VERSION
!define APP_VERSION "0.0.0.0"
!endif
!ifndef DISPLAY_VERSION
!define DISPLAY_VERSION "${APP_VERSION}"
!endif
VIProductVersion "${APP_VERSION}"
VIAddVersionKey "ProductName" "LOGAR Server"
VIAddVersionKey "CompanyName" "LOGAR"
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
VIAddVersionKey "FileDescription" "LOGAR Cloud Hub & TCP Ingestion Server Setup"
VIAddVersionKey "FileVersion" "0.3.3.0"
VIAddVersionKey "ProductVersion" "0.3.3.0"
VIAddVersionKey "FileVersion" "${APP_VERSION}"
VIAddVersionKey "ProductVersion" "${DISPLAY_VERSION}"
# --- Visual Graphics & Branding ---
!define MUI_ICON "assets/logo.ico"
@@ -26,6 +37,7 @@ VIAddVersionKey "ProductVersion" "0.3.3.0"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
Var DATADIR
Var IS_UPDATE
# --- UI Pages ---
!define MUI_ABORTWARNING
@@ -38,6 +50,7 @@ Var DATADIR
# Directory Page 1: Installation Directory
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
!insertmacro MUI_PAGE_DIRECTORY
@@ -46,6 +59,7 @@ Var DATADIR
!define MUI_PAGE_HEADER_SUBTEXT "Choose where database, certificates, and configuration files should be stored."
!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose the folder where LOGAR Server should store its database (logar_state.db), configuration (server_config.json), and mTLS certificates.$\r$\n$\r$\nDefault is recommended for automatic background service access."
!define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data & Storage Folder"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
!insertmacro MUI_PAGE_DIRECTORY
@@ -63,9 +77,43 @@ Var DATADIR
!insertmacro MUI_LANGUAGE "English"
Function SkipIfUpdate
${If} $IS_UPDATE == "1"
Abort
${EndIf}
FunctionEnd
Function .onInit
SetShellVarContext all
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
StrCpy $IS_UPDATE "0"
# 1. Mutual Exclusion Check: Deny installation if LOGAR Client is present
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Client" "DataDir"
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Client" "InstallDir"
${If} $0 != ""
${OrIf} $1 != ""
${OrIf} ${FileExists} "$PROGRAMFILES64\LOGAR\Win_Client.exe"
${OrIf} ${FileExists} "$PROGRAMFILES\LOGAR\Win_Client.exe"
MessageBox MB_ICONSTOP|MB_OK "LOGAR Client is already installed on this machine.$\r$\n$\r$\nLOGAR Server cannot be installed on a host where LOGAR Client is present (LOGAR Server already includes built-in local log collection).$\r$\n$\r$\nPlease uninstall LOGAR Client before installing LOGAR Server."
Abort
${EndIf}
# 2. Existing LOGAR Server Detection for In-Place Update Mode
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Server" "DataDir"
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Server" "InstallDir"
${If} $0 != ""
${OrIf} $1 != ""
${OrIf} ${FileExists} "$INSTDIR\Server.exe"
StrCpy $IS_UPDATE "1"
${If} $0 != ""
StrCpy $DATADIR $0
${EndIf}
${If} $1 != ""
StrCpy $INSTDIR $1
${EndIf}
MessageBox MB_ICONINFORMATION|MB_OK "An existing installation of LOGAR Server was detected.$\r$\n$\r$\nSetup will perform an in-place update: program files will be updated while your existing database, configuration (server_config.json), and certificates are preserved."
${EndIf}
FunctionEnd
Section "Install"
@@ -95,6 +143,7 @@ Section "Install"
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
WriteUninstaller "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "InstallDir" "$INSTDIR"
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "DataDir" "$DATADIR"
SectionEnd
+123 -4
View File
@@ -290,6 +290,115 @@ def ensure_libpython():
pass
import re
def get_context_version():
"""
Extracts the current release version from the build context:
1. Environment variables (APP_VERSION, LOGAR_VERSION, GITEA_REF_NAME, GITHUB_REF_NAME, GITHUB_REF)
2. Git tag via `git describe --tags --exact-match` or `git describe --tags`
3. RELEASE_NOTES.md top header
4. README.md version badge
Returns tuple: (display_version, four_part_version)
e.g. ('0.3.5.1', '0.3.5.1') or ('0.3.5', '0.3.5.0')
"""
raw_version = ""
# 1. Environment variables
for env_var in ["APP_VERSION", "LOGAR_VERSION", "GITEA_REF_NAME", "GITHUB_REF_NAME", "GITHUB_REF"]:
val = os.environ.get(env_var, "").strip()
if val:
if val.startswith("refs/tags/"):
val = val[len("refs/tags/"):]
if val.startswith("v") and len(val) > 1 and val[1].isdigit():
val = val[1:]
if any(c.isdigit() for c in val):
raw_version = val
break
# 2. Git command
if not raw_version:
try:
res = subprocess.run(["git", "describe", "--tags", "--exact-match"], cwd=ROOT_DIR, capture_output=True, text=True, check=False)
if res.returncode == 0 and res.stdout.strip():
tag = res.stdout.strip()
if tag.startswith("v") and len(tag) > 1 and tag[1].isdigit():
tag = tag[1:]
raw_version = tag
except Exception:
pass
if not raw_version:
try:
res = subprocess.run(["git", "describe", "--tags"], cwd=ROOT_DIR, capture_output=True, text=True, check=False)
if res.returncode == 0 and res.stdout.strip():
tag = res.stdout.strip().split("-")[0]
if tag.startswith("v") and len(tag) > 1 and tag[1].isdigit():
tag = tag[1:]
raw_version = tag
except Exception:
pass
# 3. RELEASE_NOTES.md
if not raw_version:
rel_notes = os.path.join(ROOT_DIR, "RELEASE_NOTES.md")
if os.path.exists(rel_notes):
try:
with open(rel_notes, "r", encoding="utf-8") as f:
content = f.read()
m = re.search(r"##\s*\[v?([0-9.]+)\]", content)
if m:
raw_version = m.group(1).strip()
except Exception:
pass
# 4. README.md
if not raw_version:
readme = os.path.join(ROOT_DIR, "README.md")
if os.path.exists(readme):
try:
with open(readme, "r", encoding="utf-8") as f:
content = f.read()
m = re.search(r"version-v?([0-9.]+)-blue", content)
if m:
raw_version = m.group(1).strip()
except Exception:
pass
# Fallback
if not raw_version:
raw_version = "0.3.5.1"
# Clean display version (strip non-standard characters, keep digits and dots)
m_clean = re.search(r"([0-9]+(?:\.[0-9]+)*)", raw_version)
display_version = m_clean.group(1) if m_clean else raw_version
# Format 4-part numeric version for NSIS VIProductVersion (X.X.X.X)
parts = display_version.split(".")
while len(parts) < 4:
parts.append("0")
four_part_version = ".".join(parts[:4])
return display_version, four_part_version
def write_version_headers(display_version, four_part_version):
"""Writes dynamic version header files for NSIS and Inno Setup compilers."""
compilation_dir = os.path.join(ROOT_DIR, "compilation")
os.makedirs(compilation_dir, exist_ok=True)
nsh_file = os.path.join(compilation_dir, "version.nsh")
with open(nsh_file, "w", encoding="utf-8") as f:
f.write(f'; Auto-generated version header from build context\n!define APP_VERSION "{four_part_version}"\n!define DISPLAY_VERSION "{display_version}"\n')
iss_file = os.path.join(compilation_dir, "version.iss")
with open(iss_file, "w", encoding="utf-8") as f:
f.write(f'; Auto-generated version header from build context\n#define AppVersion "{display_version}"\n')
print(f"[+] Dynamic context version resolved: {display_version} (NSIS VIProductVersion: {four_part_version})")
def compile_nsis_installers():
"""Compiles NSIS installers if makensis is available (native Linux/Windows)."""
makensis_bin = ensure_makensis()
@@ -297,13 +406,20 @@ def compile_nsis_installers():
print("[!] Note: NSIS compiler (makensis) not found in PATH or standard directories.")
return False
makensis_cmd = [makensis_bin]
display_ver, vi_ver = get_context_version()
write_version_headers(display_ver, vi_ver)
compilation_dir = os.path.join(ROOT_DIR, "compilation")
makensis_cmd = [
makensis_bin,
f"-DAPP_VERSION={vi_ver}",
f"-DDISPLAY_VERSION={display_ver}"
]
success = True
for nsi in ["installer_client.nsi", "installer_server.nsi"]:
nsi_path = os.path.join(compilation_dir, nsi)
if os.path.exists(nsi_path):
print(f"[*] Compiling NSIS script: {nsi} via {makensis_cmd[0]}...")
print(f"[*] Compiling NSIS script: {nsi} (v{display_ver}) via {makensis_cmd[0]}...")
try:
subprocess.run(makensis_cmd + [nsi_path], cwd=compilation_dir, check=True)
print(f"[+] Successfully compiled NSIS installer for {nsi}")
@@ -338,12 +454,15 @@ def compile_inno_setup_installers():
print("[!] Note: Inno Setup compiler (iscc / innosetup) not available on host. Skipping installer .exe creation.")
return False
display_ver, vi_ver = get_context_version()
write_version_headers(display_ver, vi_ver)
for iss in ["installer_client.iss", "installer_server.iss"]:
iss_path = os.path.join(ROOT_DIR, "compilation", iss)
if os.path.exists(iss_path):
print(f"[*] Compiling Inno Setup script: {iss}...")
print(f"[*] Compiling Inno Setup script: {iss} (v{display_ver})...")
try:
subprocess.run(iscc_cmd + [iss_path], check=True)
subprocess.run(iscc_cmd + [f"/DAppVersion={display_ver}", iss_path], check=True)
print(f"[+] Successfully compiled {iss}")
except Exception as e:
print(f"[!] Warning: Failed to compile {iss} with {iscc_cmd}: {e}")
+3
View File
@@ -6,4 +6,7 @@ uvicorn>=0.28.0
websockets>=11.0.0
pydantic>=2.6.0
distlib>=0.3.8
pyinstaller>=6.0.0
sqlalchemy>=2.0.0
requests>=2.28.0
pywin32>=306; sys_platform == "win32"
+5
View File
@@ -54,8 +54,13 @@ RUN pip3 install --no-cache-dir \
pgpy \
fastapi \
uvicorn \
websockets \
pydantic \
requests \
# Database drivers & ORM for multi-engine storage
sqlalchemy \
psycopg2-binary \
pymysql \
# Code quality / linter
flake8
+1 -1
View File
@@ -15,7 +15,7 @@ This directory contains the Docker configuration for the custom **LOGAR Build &
- **Build Essentials**:
- `gcc`, `make`, `binutils`, `build-essential`, `libffi-dev`, `libssl-dev`
- **Pre-installed Python Packages**:
- `pyinstaller`, `distlib`, `cryptography`, `pefile`, `pgpy`, `fastapi`, `uvicorn`, `pydantic`, `requests`
- `pyinstaller`, `distlib`, `cryptography`, `pefile`, `pgpy`, `fastapi`, `uvicorn`, `websockets`, `pydantic`, `requests`, `sqlalchemy`, `psycopg2-binary`, `pymysql`
---
+1853 -80
View File
File diff suppressed because it is too large Load Diff
+26 -2
View File
@@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
OID_LOGAR_SEATS = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.1")
OID_LOGAR_LICENSE_ID = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.2")
OID_LOGAR_ISSUER_URL = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.3")
OID_LOGAR_FEATURES = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.4")
@@ -448,6 +449,19 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
except (x509.ExtensionNotFound, Exception):
issuer_url = None
features = []
try:
ext_feat = cert.extensions.get_extension_for_oid(OID_LOGAR_FEATURES)
if ext_feat and ext_feat.value:
raw_val = ext_feat.value.value if hasattr(ext_feat.value, "value") else str(ext_feat.value)
feat_str = raw_val.decode("utf-8").strip() if isinstance(raw_val, bytes) else str(raw_val).strip()
if feat_str.startswith("["):
features = json.loads(feat_str)
else:
features = [f.strip() for f in feat_str.split(",") if f.strip()]
except Exception:
features = []
not_before = getattr(cert, "not_valid_before_utc", None) or cert.not_valid_before.replace(tzinfo=datetime.timezone.utc)
not_after = getattr(cert, "not_valid_after_utc", None) or cert.not_valid_after.replace(tzinfo=datetime.timezone.utc)
now = datetime.datetime.now(datetime.timezone.utc)
@@ -482,6 +496,7 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
"seats": seats,
"license_id": license_id,
"issuer_url": issuer_url,
"features": features,
"subject_cn": subject_cn,
"issuer_cn": issuer_cn,
"is_ca": is_ca,
@@ -603,11 +618,12 @@ def sign_ca_csr_with_seats(
seats: int,
days_valid: int = 365,
license_id: Optional[str] = None,
license_server_url: str = "https://logar.eibl.tech"
license_server_url: str = "https://logar.eibl.tech",
features: Optional[List[str]] = None
) -> str:
"""
Signs a subordinate CA CSR using the master license authority Root CA.
Embeds the approved seat count and license ID via custom X.509 extensions.
Embeds the approved seat count, license ID, and optional enterprise features via custom X.509 extensions.
Returns the signed PEM certificate.
"""
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
@@ -666,6 +682,13 @@ def sign_ca_csr_with_seats(
)
)
if features:
feat_str = json.dumps(features) if isinstance(features, list) else str(features)
builder = builder.add_extension(
x509.UnrecognizedExtension(OID_LOGAR_FEATURES, feat_str.encode("utf-8")),
critical=False
)
signed_cert = builder.sign(master_ca_key, hashes.SHA256())
return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
@@ -903,6 +926,7 @@ def import_license_package(
"master_root_ca_pem": master_root_ca_pem,
"license_info": info,
"seats": info.get("seats"),
"features": info.get("features", []),
"license_id": info.get("license_id"),
"issuer_url": info.get("issuer_url"),
"fingerprint": info.get("fingerprint"),
+413
View File
@@ -470,8 +470,421 @@ class TestServerComponent(unittest.TestCase):
self.assertEqual(client_data["active_seats"], 0)
self.assertEqual(len(client_data["clients"]), 1)
def test_two_factor_auth_and_totp_flow(self):
"""Tests TOTP secret generation, token verification, URI generation, and enable/disable lifecycle."""
Server.init_db(self.test_db)
# 1. Test TOTP helpers
secret = Server.generate_totp_secret()
self.assertTrue(len(secret) >= 16)
token = Server.get_totp_token(secret)
self.assertEqual(len(token), 6)
self.assertTrue(token.isdigit())
self.assertTrue(Server.verify_totp_code(secret, token))
self.assertFalse(Server.verify_totp_code(secret, "000000" if token != "000000" else "999999"))
self.assertFalse(Server.verify_totp_code(secret, "invalid"))
# 2. Test OTPAuth URI
uri = Server.get_totp_uri("admin", secret, issuer="LOGAR Fleet Hub")
self.assertTrue(uri.startswith("otpauth://totp/"))
self.assertIn("secret=" + secret, uri)
self.assertIn("LOGAR", uri)
# 3. Test initial user 2FA status
info = Server.get_user_totp_info("admin", db_path=self.test_db)
self.assertFalse(info["enabled"])
self.assertEqual(info["secret"], "")
# 4. Test enabling 2FA
enabled = Server.enable_user_totp("admin", secret, db_path=self.test_db)
self.assertTrue(enabled)
info_active = Server.get_user_totp_info("admin", db_path=self.test_db)
self.assertTrue(info_active["enabled"])
self.assertEqual(info_active["secret"], secret)
# 5. Test disabling 2FA
disabled = Server.disable_user_totp("admin", db_path=self.test_db)
self.assertTrue(disabled)
info_disabled = Server.get_user_totp_info("admin", db_path=self.test_db)
self.assertFalse(info_disabled["enabled"])
def test_persistent_session_secret_and_rate_limiting(self):
"""Tests DB persistence of session secret and sliding window rate limiting."""
Server.init_db(self.test_db)
# 1. Session secret persistence
sec1 = Server.get_session_secret(self.test_db)
self.assertTrue(len(sec1) >= 32)
conn = sqlite3.connect(self.test_db)
c = conn.cursor()
c.execute("SELECT value FROM server_meta WHERE key = 'session_secret'")
row = c.fetchone()
conn.close()
self.assertIsNotNone(row)
self.assertEqual(row[0], sec1)
# Token generated with test_db
token = Server.generate_session_token("admin", db_path=self.test_db)
self.assertEqual(Server.verify_session_token(token, db_path=self.test_db), "admin")
# 2. Rate limiting
test_ip = "10.0.0.99"
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
self.assertFalse(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
# Different scope ('enroll') should still be allowed
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="enroll", db_path=self.test_db))
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
def test_ingest_queue_manager_and_batch_flush(self):
"""Tests IngestQueueManager buffering, capacity bounds, and atomic batch flushing."""
Server.init_db(self.test_db)
qm = Server.IngestQueueManager(flush_interval=1.0, batch_size=5, max_queue=50)
# Initial stats
stats = qm.get_stats()
self.assertEqual(stats["queue_size"], 0)
self.assertEqual(stats["total_ingested"], 0)
self.assertEqual(stats["total_flushed"], 0)
# Push 3 payloads
for i in range(3):
payload = {
"server": f"node-{i}.corp.local",
"logs": [
{"server": f"node-{i}", "signature": f"Sig_{i}", "severity": "WARNING", "message": f"Log msg {i}", "os_type": "linux"}
]
}
pushed = qm.push(payload)
self.assertTrue(pushed)
stats = qm.get_stats()
self.assertEqual(stats["queue_size"], 3)
self.assertEqual(stats["total_ingested"], 3)
# Flush to database
res_flush = qm.flush(db_path=self.test_db, window_hours=12, min_runs=1)
self.assertEqual(res_flush["flushed_runs"], 3)
self.assertEqual(res_flush["processed_logs"], 3)
# Verify DB records
conn = sqlite3.connect(self.test_db)
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM ingest_runs")
run_count = c.fetchone()[0]
c.execute("SELECT COUNT(*) FROM active_issues WHERE status = 'VERIFIED'")
issue_count = c.fetchone()[0]
conn.close()
self.assertEqual(run_count, 3)
self.assertEqual(issue_count, 3)
# Stats after flush
stats = qm.get_stats()
self.assertEqual(stats["queue_size"], 0)
self.assertEqual(stats["total_flushed"], 3)
def test_enterprise_db_feature_gating(self):
"""Tests feature check helpers and DB storage info under standard and enterprise licensed states."""
Server.init_db(self.test_db)
# 1. Standard / Unlicensed state
self.assertFalse(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
info = Server.get_storage_engine_info(db_path=self.test_db)
self.assertEqual(info["engine"], "sqlite")
self.assertFalse(info["feature_unlocked"])
self.assertIn("SQLite 3 (WAL Mode)", info["backend_display"])
# 2. Unlock enterprise_db feature in DB
conn = sqlite3.connect(self.test_db)
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db", "ai_heuristics"]),))
conn.commit()
conn.close()
self.assertTrue(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
self.assertTrue(Server.is_feature_licensed("ai_heuristics", db_path=self.test_db))
self.assertFalse(Server.is_feature_licensed("unknown_feat", db_path=self.test_db))
info_unlocked = Server.get_storage_engine_info(db_path=self.test_db)
self.assertTrue(info_unlocked["feature_unlocked"])
self.assertIn("enterprise_db", info_unlocked["licensed_features"])
def test_postgres_storage_endpoints_authorization(self):
"""Tests REST API security gating on DB engine management endpoints."""
Server.init_db(self.test_db)
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "storage_engine": "sqlite"}
# 1. Test direct authorization & endpoint logic
stats = Server.api_db_stats()
self.assertIn("queue", stats)
self.assertIn("storage", stats)
self.assertFalse(stats["enterprise_db_licensed"])
# 2. Test Connection without enterprise_db -> HTTPException 403
pg_req = Server.DbConnectionTestRequest(host="localhost", port=5432, database="logar", user="postgres", password="")
with self.assertRaises(Server.HTTPException) as cm:
Server.api_db_test_connection(pg_req)
self.assertEqual(cm.exception.status_code, 403)
self.assertIn("enterprise_db", cm.exception.detail)
# 3. Switch backend without enterprise_db -> HTTPException 403
switch_req = Server.DbSwitchBackendRequest(db_type="postgres", host="localhost", port=5432, database="logar", user="postgres", password="")
with self.assertRaises(Server.HTTPException) as cm:
Server.api_db_switch_backend(switch_req)
self.assertEqual(cm.exception.status_code, 403)
self.assertIn("enterprise_db", cm.exception.detail)
# 4. Flush buffer endpoint executes successfully
flush_res = Server.api_db_flush_buffer()
self.assertEqual(flush_res["status"], "success")
# 5. Optional HTTP client verification if starlette testclient dependencies are present
try:
from fastapi.testclient import TestClient
client = TestClient(Server.app)
# Unauthenticated request to /api/db/stats -> 401
res = client.get("/api/db/stats")
self.assertEqual(res.status_code, 401)
# Authenticated request with session cookie
token = Server.generate_session_token("admin", db_path=self.test_db)
client.cookies.set(Server.SESSION_COOKIE_NAME, token)
res_auth = client.get("/api/db/stats")
self.assertEqual(res_auth.status_code, 200)
data = res_auth.json()
self.assertIn("queue", data)
self.assertIn("storage", data)
self.assertFalse(data["enterprise_db_licensed"])
# 403 checks via HTTP client
res_test = client.post("/api/db/test-connection", json={"host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
self.assertEqual(res_test.status_code, 403)
res_switch = client.post("/api/db/switch-backend", json={"db_type": "postgres", "host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
self.assertEqual(res_switch.status_code, 403)
res_flush = client.post("/api/db/flush-buffer")
self.assertEqual(res_flush.status_code, 200)
except (ImportError, RuntimeError, Exception):
# Starlette TestClient may raise RuntimeError when httpx/httpx2 is missing in lightweight runner environments
pass
def test_db_encryption_at_rest_and_full_config_api(self):
"""Tests OpenPGP data-at-rest encryption/decryption and full DB/Cache configuration API."""
Server.init_db(self.test_db)
priv_armored, pub_armored, fp = Server.generate_server_keypair("test-db-node")
priv_key, _ = pgpy.PGPKey.from_blob(priv_armored)
pub_key, _ = pgpy.PGPKey.from_blob(pub_armored)
Server.SERVER_STATE["config"] = {
"db_path": self.test_db,
"storage_engine": "sqlite",
"cache_mode": "direct",
"encrypt_at_rest": False,
"private_key": priv_armored,
"public_key": pub_armored,
"server_fingerprint": fp
}
Server.SERVER_STATE["private_key_obj"] = priv_key
Server.SERVER_STATE["public_key_obj"] = pub_key
# 1. Test OpenPGP encryption & decryption roundtrip
raw_msg = "Critical database corruption: segment 0x89FA damaged"
encrypted_msg = Server.encrypt_field_for_storage(raw_msg, force=True)
self.assertTrue(encrypted_msg.startswith("-----BEGIN PGP MESSAGE-----"))
self.assertNotIn("Critical database corruption", encrypted_msg)
decrypted_msg = Server.decrypt_field_from_storage(encrypted_msg)
self.assertEqual(decrypted_msg, raw_msg)
# 2. Test that without enterprise_db license, buffering, encryption at rest, and postgres are blocked
cfg_unlicensed_buf = Server.DbFullConfigRequest(
storage_engine="sqlite",
cache_mode="buffered",
encrypt_at_rest=False
)
with self.assertRaises(Server.HTTPException) as cm:
Server.api_db_update_config(cfg_unlicensed_buf)
self.assertEqual(cm.exception.status_code, 403)
self.assertIn("enterprise_db", cm.exception.detail)
cfg_unlicensed_enc = Server.DbFullConfigRequest(
storage_engine="sqlite",
cache_mode="direct",
encrypt_at_rest=True
)
with self.assertRaises(Server.HTTPException) as cm:
Server.api_db_update_config(cfg_unlicensed_enc)
self.assertEqual(cm.exception.status_code, 403)
self.assertIn("enterprise_db", cm.exception.detail)
pg_req = Server.DbFullConfigRequest(
storage_engine="postgres",
cache_mode="direct",
pg_config=Server.DbConnectionTestRequest(host="db.corp.internal", port=5432, database="logar", user="postgres", password="")
)
with self.assertRaises(Server.HTTPException) as cm:
Server.api_db_update_config(pg_req)
self.assertEqual(cm.exception.status_code, 403)
self.assertIn("enterprise_db", cm.exception.detail)
# 3. Standard local SQLite configuration succeeds when unlicensed
cfg_standard = Server.DbFullConfigRequest(
storage_engine="sqlite",
cache_mode="direct",
encrypt_at_rest=False
)
res_std = Server.api_db_update_config(cfg_standard)
self.assertEqual(res_std["status"], "success")
# 4. Unlock enterprise_db feature in license_config
conn = sqlite3.connect(self.test_db)
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),))
conn.commit()
conn.close()
# 5. Test updating config with enterprise_db unlocked (direct -> buffered, encrypt_at_rest = True)
cfg_req = Server.DbFullConfigRequest(
storage_engine="sqlite",
cache_mode="buffered",
flush_interval=4.0,
batch_size=200,
max_queue=25000,
encrypt_at_rest=True
)
res = Server.api_db_update_config(cfg_req)
self.assertEqual(res["status"], "success")
self.assertEqual(Server.SERVER_STATE["config"]["cache_mode"], "buffered")
self.assertTrue(Server.SERVER_STATE["config"]["encrypt_at_rest"])
self.assertEqual(Server.INGEST_QUEUE.flush_interval, 4.0)
self.assertEqual(Server.INGEST_QUEUE.batch_size, 200)
# 6. Verify storage engine info output
info = Server.get_storage_engine_info(db_path=self.test_db)
self.assertEqual(info["cache_mode"], "buffered")
self.assertTrue(info["encrypt_at_rest"])
self.assertEqual(info["flush_interval"], 4.0)
def test_active_licensed_features_helper(self):
"""Tests helper function to extract active licensed capabilities from DB and certificates."""
Server.init_db(self.test_db)
Server.SERVER_STATE["ca_cert_pem"] = ""
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": "non_existent_test_certs"}
# 1. Unlicensed state
features_unlicensed = Server.get_active_licensed_features(db_path=self.test_db)
self.assertEqual(features_unlicensed, [])
self.assertFalse(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
# 2. Add features via JSON string in DB
conn = sqlite3.connect(self.test_db)
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db", "ai_heuristics"]),))
conn.commit()
conn.close()
features_licensed = Server.get_active_licensed_features(db_path=self.test_db)
self.assertEqual(features_licensed, ["ai_heuristics", "enterprise_db"])
self.assertTrue(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
self.assertTrue(Server.is_feature_licensed("ai_heuristics", db_path=self.test_db))
self.assertFalse(Server.is_feature_licensed("non_existent_feature", db_path=self.test_db))
# 3. Verify get_license_summary includes features list
summary = Server.get_license_summary()
self.assertIn("features", summary)
self.assertIn("enterprise_db", summary["features"])
def test_webui_tab_rendering_and_feature_cards(self):
"""Tests WebUI HTML generation for clean tab buttons and feature cards in PKI tab."""
Server.init_db(self.test_db)
Server.SERVER_STATE["config"] = {
"db_path": self.test_db,
"cert_dir": "certs",
"tcp_port": 9443,
"hermes_port": 8443,
"server_name": "Test-Hub"
}
# 1. Render WebUI in standard / unlicensed mode
resp = Server.serve_webui(request=None)
html_content = resp.body.decode("utf-8")
# Verify clean tab buttons without number suffixes
self.assertIn('>💻 Fleet Forwarders</button>', html_content)
self.assertIn('>🤖 Hermes AI & Tokens</button>', html_content)
self.assertIn('>📊 Verified Telemetry</button>', html_content)
self.assertIn('>📜 Sub-CA & Licensing</button>', html_content)
self.assertIn('>💾 DB Engine</button>', html_content)
self.assertNotIn('🟢 UNLOCKED', html_content)
self.assertNotIn('Fleet Forwarders (', html_content)
# Verify Licensed Capabilities card is present
self.assertIn('Licensed Capabilities &amp; Feature Modules', html_content)
self.assertIn('Standard Community Edition is running', html_content)
# 2. Render WebUI with enterprise_db unlocked
conn = sqlite3.connect(self.test_db)
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),))
conn.commit()
conn.close()
resp_ent = Server.serve_webui(request=None)
html_ent = resp_ent.body.decode("utf-8")
self.assertIn('enterprise_db', html_ent)
self.assertIn('Enterprise Database &amp; High-Throughput Engine', html_ent)
self.assertIn('>💾 DB Engine</button>', html_ent)
self.assertNotIn('🟢 UNLOCKED', html_ent)
def test_explicit_opt_in_guarantee(self):
"""Tests that licensing does NOT automatically turn on encryption or buffering without explicit admin setting."""
Server.init_db(self.test_db)
priv_armored, pub_armored, fp = Server.generate_server_keypair("test-opt-in-node")
priv_key, _ = pgpy.PGPKey.from_blob(priv_armored)
pub_key, _ = pgpy.PGPKey.from_blob(pub_armored)
Server.SERVER_STATE["config"] = {
"db_path": self.test_db,
"cert_dir": "certs",
"storage_engine": "sqlite",
"cache_mode": "direct",
"encrypt_at_rest": False,
"private_key": priv_armored,
"public_key": pub_armored,
"server_fingerprint": fp
}
Server.SERVER_STATE["private_key_obj"] = priv_key
Server.SERVER_STATE["public_key_obj"] = pub_key
# Unlock enterprise license
conn = sqlite3.connect(self.test_db)
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),))
conn.commit()
conn.close()
# License is active, but config has encrypt_at_rest=False -> plaintext storage
raw_msg = "Sample operational log event"
result_msg = Server.encrypt_field_for_storage(raw_msg)
self.assertEqual(result_msg, raw_msg)
self.assertFalse(result_msg.startswith("-----BEGIN PGP MESSAGE-----"))
# Ingestion cache mode is direct
self.assertEqual(Server.SERVER_STATE["config"]["cache_mode"], "direct")
if __name__ == "__main__":
unittest.main()