Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0edae4d797 | ||
|
|
cc25c31884 | ||
|
|
421eafb72d | ||
|
|
ca8b0153fa | ||
|
|
50f76ca3c3 | ||
|
|
ea6410d391 | ||
|
|
41d7d66a56 | ||
|
|
959f2977f0 | ||
|
|
40cdbf349c | ||
|
|
ca1b5b5d25 | ||
|
|
c1d4675321 | ||
|
|
2d33b14d60 | ||
|
|
2b0b680188 | ||
|
|
f0bf90e033 | ||
|
|
22072a0b88 | ||
|
|
730f0ba7ca | ||
|
|
b7c27ee3a1 | ||
|
|
fa662da476 | ||
|
|
6cb9631958 | ||
|
|
c03bb348b5 | ||
|
|
2d39f3c007 | ||
|
|
78e62b31d2 | ||
|
|
860e39b360 | ||
|
|
2a04244db3 | ||
|
|
9f999fc6c3 | ||
|
|
3399c6d57b |
@@ -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."
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+1
-1
Submodule LOGAR-LICENSING updated: a109af5f4d...38a41cfbac
@@ -5,14 +5,14 @@
|
||||
# LOGAR: Fleet Log Aggregation & Centralized Provisioning System
|
||||
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
|
||||
|
||||
[](RELEASE_NOTES.md)
|
||||
[](tests/)
|
||||
[](RELEASE_NOTES.md)
|
||||
[](tests/)
|
||||
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||
[](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)
|
||||
|
||||
+25
-38
@@ -1,46 +1,33 @@
|
||||
# LOGAR Release Notes
|
||||
|
||||
## [v0.3.4.1] - 2026-09-27
|
||||
## [v0.3.5.1] - 2026-09-30
|
||||
|
||||
### 🎨 Installer Dialog UI Spacing & Branding Enhancements
|
||||
- **NSIS Client Config Profile Dialog Fix**: Adjusted vertical coordinates and label boundaries in the Windows Client NSIS installer (`installer_client.nsi`) to prevent input field overlap and ensure multi-line instructional text renders without visual clipping.
|
||||
- **Installer Footer Branding Clean-Up**: Escaped ampersands in Windows installer branding labels (`BrandingText`) to avoid character truncation in native win32 footer panels.
|
||||
- **License Authority UI Clean-Up**: Simplified table ACTIONS column in the Licensing Authority portal to exclusively display `⚙️ Manage`, keeping secondary actions (`Reinstate`, `Delete`) inside the dedicated management modal.
|
||||
### 🗄️ 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.4] - 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.
|
||||
|
||||
### 🔄 In-Place Installer Upgrades & Mutual Exclusion Guards
|
||||
- **Seamless In-Place Upgrades**: Windows (Inno Setup & NSIS) and Linux installers now detect existing installations automatically. In upgrade mode, installers stop background services, update core binaries and runtime in-place, and restart services without overwriting configuration files (`server_config.json`, `client_config.json`), database state (`logar_state.db`), state tracking (`client_state.json`), or mTLS certificates.
|
||||
- **Host Mutual Exclusion Validation**: Added pre-installation validation across all installers to prevent accidental co-installation of LOGAR Server and LOGAR Client on the same host, showing descriptive guidance to administrators.
|
||||
### 📜 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.
|
||||
|
||||
### 🛡️ WebUI 2FA & Telemetry Stability
|
||||
- **Server Hub TOTP Two-Factor Authentication**: Implemented RFC 6238 TOTP 2FA for the LOGAR Server Hub web portal (`:8443`), featuring interactive QR code setup, secret generation, and secure session validation.
|
||||
- **Telemetry Stream UI Buffer Capping**: Limited the live verified telemetry stream view to the latest 50 events in the Server Hub dashboard for enhanced browser performance and UI stability.
|
||||
### 🛠️ 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.
|
||||
|
||||
### 🔑 Licensing Authority Management & Registry Tooling
|
||||
- **Revocation, Reinstatement & License Deletion**: Enhanced the Master Licensing Authority (`logar.eibl.tech`) with instant license revocation, reinstatement of revoked licenses, and conditional permanent deletion of revoked records.
|
||||
- **Interactive Capability Explorer**: Added interactive implementation breakdown modals across core capabilities on the public project landing page.
|
||||
- **Automated Gitea Container Pruning**: Added `prune_images.py` utility to maintain clean container registries by keeping only the latest active image versions.
|
||||
|
||||
---
|
||||
|
||||
## [v0.3.3] - 2026-09-27
|
||||
|
||||
### 🎨 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.
|
||||
|
||||
---
|
||||
|
||||
## [v0.3.2] - 2026-09-27
|
||||
|
||||
### 🪟 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.
|
||||
### 📖 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.
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
#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.4.1
|
||||
AppVersion={#AppVersion}
|
||||
AppPublisher=LOGAR
|
||||
AppPublisherURL=https://logar.eibl.tech
|
||||
AppSupportURL=https://logar.eibl.tech
|
||||
@@ -19,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.4.1
|
||||
VersionInfoVersion={#AppVersion}
|
||||
VersionInfoProductName=LOGAR Client
|
||||
VersionInfoProductVersion=0.3.4.1
|
||||
VersionInfoProductVersion={#AppVersion}
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
@@ -9,13 +9,24 @@ RequestExecutionLevel admin
|
||||
|
||||
BrandingText "LOGAR Centralized Log Aggregator && Forwarder"
|
||||
|
||||
VIProductVersion "0.3.4.1"
|
||||
!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.4.1"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.1"
|
||||
VIAddVersionKey "FileVersion" "${APP_VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${DISPLAY_VERSION}"
|
||||
|
||||
# --- Visual Graphics & Branding ---
|
||||
!define MUI_ICON "assets/logo.ico"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
#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.4.1
|
||||
AppVersion={#AppVersion}
|
||||
AppPublisher=LOGAR
|
||||
AppPublisherURL=https://logar.eibl.tech
|
||||
AppSupportURL=https://logar.eibl.tech
|
||||
@@ -19,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.4.1
|
||||
VersionInfoVersion={#AppVersion}
|
||||
VersionInfoProductName=LOGAR Server
|
||||
VersionInfoProductVersion=0.3.4.1
|
||||
VersionInfoProductVersion={#AppVersion}
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
@@ -8,13 +8,24 @@ RequestExecutionLevel admin
|
||||
|
||||
BrandingText "LOGAR Centralized Log Aggregator && Ingestion Hub"
|
||||
|
||||
VIProductVersion "0.3.4.1"
|
||||
!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.4.1"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.1"
|
||||
VIAddVersionKey "FileVersion" "${APP_VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${DISPLAY_VERSION}"
|
||||
|
||||
# --- Visual Graphics & Branding ---
|
||||
!define MUI_ICON "assets/logo.ico"
|
||||
|
||||
+123
-4
@@ -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}")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1461
-69
File diff suppressed because it is too large
Load Diff
@@ -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"),
|
||||
|
||||
@@ -510,8 +510,381 @@ class TestServerComponent(unittest.TestCase):
|
||||
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 & 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 & 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()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user