142 Commits
Author SHA1 Message Date
me0nline 37f959e402 feat(licensing): add WebUI license import modal, remove lic step from setup, default unlicensed to 0 seats
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m4s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m38s
2026-09-26 20:50:57 +02:00
me0nline f1e5e8f986 docs: clarify default WebUI credentials in installation section of README
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m16s
2026-09-26 20:44:17 +02:00
me0nline 5a0fdb4374 feat(installer): add .lic license file initialization step during installation on Windows and Linux
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 7m16s
2026-09-26 20:29:54 +02:00
me0nline fdfa8dfe7f feat(installer): add custom data directory selection page and enterprise branding to client and server setups
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-26 20:22:59 +02:00
me0nline b40966ac4c fix(database, installer): enable WAL mode, grant ProgramData permissions in NSIS, and fallback to LocalAppData when read-only
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m21s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 8m5s
2026-09-26 20:21:14 +02:00
me0nline e47fe609a6 fix(installer): ensure complete cleanup of certs, configs, database, logs, and directories upon uninstallation
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m12s
2026-09-26 19:58:15 +02:00
me0nline 48bb279601 fix(server, client): auto-resolve db, cert, state and config paths to ProgramData when running in read-only directories
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 2m2s
2026-09-26 19:56:37 +02:00
me0nline fd34fe9911 fix(packaging): bundle embedded python runtime and dependencies for standalone Windows distribution
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m9s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 7m5s
2026-09-26 19:42:12 +02:00
me0nline a7ff1c845d fix(packaging): set valid interpreter shebang in zipapp launcher and ensure standalone PE binaries
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m36s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 3m56s
2026-09-26 19:05:57 +02:00
me0nline b0066c9ea6 security(runner): remove plaintext token fallbacks, untrack .env, and enforce secret-only authentication
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m49s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 3m50s
2026-09-26 18:57:06 +02:00
me0nline 079c6cb70f ci(runner): restrict build-runner trigger to manual dispatch and release published
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-26 18:55:57 +02:00
me0nline 49525594a3 ci: switch actions/checkout to v3 for compatibility with act_runner Node runtime
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 3m32s
Build and Push LOGAR Runner / Build & Push Runner Image (push) Canceled after 0s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 3m50s
2026-09-26 18:52:38 +02:00
me0nline 04ccc999a8 ci(runner): clean up Dockerfile by stripping unused Java, PowerShell, SpotBugs, and Clang toolchains
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m3s
Build and Push LOGAR Runner / Build & Push Runner Image (push) Successful in 11m40s
2026-09-26 18:45:04 +02:00
me0nline 267aa54076 ci(runner): adopt exact CodeChecker Dockerfile base and append NSIS/Python build toolchains
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
Build and Push LOGAR Runner / Build & Push Runner Image (push) Canceled after 0s
2026-09-26 18:44:18 +02:00
me0nline 2c9f8d9b91 ci(runner): make runner image completely standalone based on ubuntu:22.04
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m10s
Build and Push LOGAR Runner / Build & Push Runner Image (push) Successful in 6m30s
2026-09-26 18:43:51 +02:00
me0nline 125870b135 ci(runner): align workflow with CodeChecker pipeline template and pull base image before build
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 7s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Failing after 6s
Build and Push LOGAR Runner / Build & Push Runner Image (push) Canceled after 0s
2026-09-26 18:38:36 +02:00
me0nline f0df5a6e4d ci(runner): base Dockerfile on existing codechecker-runner image for fast lightweight layer builds
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 29s
Build and Push LOGAR Runner / Build & Push LOGAR Runner Image (push) Canceled after 6m44s
2026-09-26 18:32:12 +02:00
me0nline 5b59b18c2c ci(runner): install nodejs and npm in runner image for Gitea Actions runtime support
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 9s
Build and Push LOGAR Runner / Build & Push LOGAR Runner Image (push) Canceled after 11m31s
2026-09-26 18:21:30 +02:00
me0nline ec80908d5b ci(runner): update Gitea container registry auth to use GITEATOKEN secret
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 0s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Failing after 0s
Build and Push LOGAR Runner / Build & Push LOGAR Runner Image (push) Canceled after 0s
2026-09-26 18:07:20 +02:00
me0nline 92d858841d ci(runner): scope runner label strictly to logar and update release/ci workflows
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Failing after 0s
Build and Push LOGAR Runner / Build & Push LOGAR Runner Image (push) Canceled after 0s
2026-09-26 18:06:34 +02:00
me0nline 0e616d614c ci(runner): configure Portainer webhook and inject registration token via runner .env
Build and Push LOGAR Runner / Build & Push LOGAR Runner Image (push) Failing after 40s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 2m8s
2026-09-26 18:05:55 +02:00
me0nline 38b36c28a0 ci(runner): add Portainer docker-compose stack and deployment guide for Act Runner
Build and Push LOGAR Runner / Build & Push LOGAR Runner Image (push) Failing after 42s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m59s
2026-09-26 17:43:38 +02:00
me0nline c51194e699 ci(runner): add custom Docker runner and build-and-push workflow for Gitea registry
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 3m18s
Build and Push LOGAR Runner / Build & Push LOGAR Runner Image (push) Failing after 1m0s
2026-09-26 17:39:33 +02:00
me0nline 3a3d251817 ci(release): add apt -f install and fix-broken resolution for Debian runner packages
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 1m39s
2026-09-26 14:20:26 +02:00
me0nline 25adb46d48 ci(release): flush apt cache and force fresh mirror package index to fix 404 security packages
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m32s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 1m33s
2026-09-26 14:06:35 +02:00
me0nline b33294f36c ci(release): add dynamic fallback package resolution and support suite changes in runner
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 1m34s
2026-09-26 13:57:09 +02:00
me0nline b8069ee66b ci(release): enhance Debian package installation loop and makensis binary resolution
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 1m38s
2026-09-26 13:49:58 +02:00
me0nline d025689193 refactor(quality): clean up unused imports, variables, and redundant f-strings
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m36s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 1m14s
2026-09-26 13:43:01 +02:00
me0nline 96126cdf84 fix(ci): dynamically install runner python shared lib and nsis for reliable installer generation
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m47s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 1m9s
2026-09-26 13:38:04 +02:00
me0nline 17c1791b7d fix(ci): add native NSIS installer compilation and libpython3 dev dependencies for Linux runner
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m45s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 1m10s
2026-09-26 13:33:19 +02:00
me0nline 911014fae7 chore(release): bump version to v0.3.1
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m36s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 1m22s
2026-09-26 13:23:36 +02:00
me0nline de95a3283d ci(release): automate Inno Setup & Linux installer packaging directly inside CI pipeline
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s
2026-09-26 13:21:14 +02:00
me0nline b0bf322275 feat(packaging): include Inno Setup installers, Linux scripts, and bundles in release distribution
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m29s
2026-09-26 13:20:41 +02:00
me0nline a496b7e822 docs: clearly present the 4 automated service installers for Linux and Windows
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m39s
2026-09-26 13:16:58 +02:00
me0nline 29784e166f docs: clarify standalone binary usage in Quick Start without requiring python/pip
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s
2026-09-26 13:12:41 +02:00
me0nline 56844983e2 docs: streamline README into concise summary with wiki links
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m39s
2026-09-26 13:09:42 +02:00
me0nline 07b8f48631 docs: add direct link to Gitea Wiki documentation
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m36s
2026-09-26 13:07:55 +02:00
me0nline fa8b9e0af5 docs: update README with v3.0.1 architecture, 35 unit tests, and cross-compilation
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m41s
2026-09-26 13:05:19 +02:00
me0nline 93d9f0e463 feat(security): apply html.escape sanitization across WebUI table rendering
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s
2026-09-26 13:00:07 +02:00
me0nline 41cb956416 feat(security): enforce chmod 700 on certs directory in linux installer
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m40s
2026-09-26 12:55:31 +02:00
me0nline 42916dc8ee docs: add release notes for v3.0.1
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 1m4s
2026-09-25 23:55:05 +02:00
me0nline a34934959d fix(warnings): align OpenPGP compression preferences and suppress cryptography deprecations in CI
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m34s
2026-09-25 23:54:28 +02:00
me0nline fe296b42e7 chore(submodule): update LOGAR-LICENSING pointer
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m37s
2026-09-25 23:51:48 +02:00
me0nline e04082d4b8 feat(security): implement IP rate limiting and secure session cookies on Hub
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m46s
2026-09-25 23:50:15 +02:00
me0nline ad022cc447 sec: enforce admin authorization across all management endpoints and randomize session secret
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m0s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
2026-09-25 23:39:37 +02:00
me0nline 344d57ff49 ci(release): cross-compile and publish both Linux and Windows release binaries from Linux node
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m32s
2026-09-25 23:24:29 +02:00
me0nline 19c60616b1 fix(release): dynamically locate Git and provide API zip archive fallback on Windows runner
Release Binaries & Installers / Build & Release Linux Binaries (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 1m34s
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Canceled after 9m33s
2026-09-25 23:16:36 +02:00
me0nline 14b3586b10 fix(release): install python shared libraries for PyInstaller and add zipapp fallback
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Failing after 30s
Release Binaries & Installers / Build & Release Linux Binaries (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 0s
2026-09-25 23:15:31 +02:00
me0nline a9d5d641cd chore(submodule): update LOGAR-LICENSING to include httpx test dependency
CI Test Suite / Run Component Tests & Pipeline Verification (push) Canceled after 0s
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Failing after 34s
Release Binaries & Installers / Build & Release Linux Binaries (push) Canceled after 1m7s
2026-09-25 23:14:48 +02:00
me0nline 1d8449515e fix(ci): use public /health probe and authorize Hermes report requests in pipeline test
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m50s
Release Binaries & Installers / Build & Release Linux Binaries (push) Failing after 1m47s
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Canceled after 3m20s
2026-09-25 23:08:29 +02:00
me0nline 668f6e66cd chore(release): bump version to v3.0.0 and prepare unified release
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m50s
Release Binaries & Installers / Build & Release Linux Binaries (push) Failing after 1m5s
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Canceled after 0s
2026-09-25 23:05:54 +02:00
me0nline 6ac2c89aac Sync active seat usage with upstream authority and streamline WebUI dashboard
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 2m0s
2026-09-25 23:02:14 +02:00
me0nline cce439f0e2 ci: remove submodule dependency from LOGAR CI and release workflows so unit tests run standalone
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m53s
2026-09-25 22:56:01 +02:00
me0nline 2c6265b6cc chore(submodule): bump LOGAR-LICENSING to include container healthcheck fix
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 19s
2026-09-25 22:54:36 +02:00
me0nline 4396052ed3 feat(licensing): report active seat quota and usage to upstream license authority
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s
2026-09-25 22:53:26 +02:00
me0nline c2fed0379c feat(webui): streamline hub UI by removing CSR/lic export/import options and license management header title
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 13s
2026-09-25 22:47:35 +02:00
me0nline e01421ea8c fix(client): resolve hermes_host 0.0.0.0 bind addresses to dialable server host during auto-enrollment
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-25 22:44:20 +02:00
me0nline 2e10aa4819 test(licensing): decouple LOGAR unit tests from private submodule and use standalone mock certificates
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 16s
2026-09-25 22:41:57 +02:00
me0nline 777d6300a8 ci: enable recursive submodule checkout and fallback init in ci and release workflows
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-25 22:37:32 +02:00
me0nline c3bb440eff ci: extract pip bootstrapping into compilation/ensure_pip.py to fix yaml scalar parsing
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m29s
2026-09-25 22:35:21 +02:00
me0nline ea3a2cd541 ci: support python 3.9 legacy get-pip bootstrap endpoint dynamically 2026-09-25 22:33:08 +02:00
me0nline 8c6d3f57f9 ci: use pure python dependency management and eliminate apt package conflicts in runners
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 10s
2026-09-25 22:29:08 +02:00
me0nline f7584320f9 ci: harden debian container package installation and pip bootstrapping against stale apt mirror lists
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 42s
2026-09-25 22:27:50 +02:00
me0nline d9d895e468 feat: aggregation server info webui with client profile export, seat management, last activity time, auth, and hermes tokens
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 17s
2026-09-25 22:26:32 +02:00
me0nline 4c8db1ec94 Split state machines into modular, focused subsections for PKI enrollment, sync, forwarder discovery, and mTLS streaming
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 19s
2026-09-25 22:12:37 +02:00
me0nline 5fe74325ec Simplify main state machine and add dedicated subsections for licensing, forwarder, and temporal state machines
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-25 22:10:50 +02:00
me0nline 871d3c15ab Update README with Log-Zusammenfuehrung & Bereitstellung focus and full PKI licensing state machine
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 20s
2026-09-25 22:08:35 +02:00
me0nline 7c57cf0645 Update LOGAR-LICENSING with clean 2FA badges without emoji icons
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s
2026-09-25 22:04:47 +02:00
me0nline 2d75b595e6 Update LOGAR-LICENSING with gear manage button and non-clickable license ID
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 20s
2026-09-25 21:57:55 +02:00
me0nline d152b65f6e Update LOGAR-LICENSING with table row bottom border alignment fix
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 20s
2026-09-25 21:57:11 +02:00
me0nline eced9bc5a1 Update LOGAR-LICENSING with 2FA ON/OFF badge
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s
2026-09-25 21:52:39 +02:00
me0nline 3292702dd3 Update LOGAR-LICENSING submodule with 2FA TOTP support and header cleanup
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 20s
2026-09-25 21:42:18 +02:00
me0nline fac6ae8847 feat(hub): report seats_taken in WebSocket license sync and telemetry
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 22s
2026-09-25 21:30:45 +02:00
me0nline f4996a53cc Update LOGAR-LICENSING submodule pointer with seat decrease and quota setting capabilities
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 20s
2026-09-25 21:23:41 +02:00
me0nline 05f703b024 Fix cdata keyword argument to cadata in ssl.load_verify_locations
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-25 21:20:09 +02:00
me0nline eb628ff40b Update LOGAR-LICENSING submodule pointer with Admin tab and user dropdown enhancements
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s
2026-09-25 21:15:42 +02:00
me0nline 79a2720363 Update LOGAR-LICENSING submodule pointer with startup and session DB init fix
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 20s
2026-09-25 16:35:14 +02:00
me0nline 27de5e9e06 Update LOGAR-LICENSING submodule pointer to include user management and tenants
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-25 16:22:31 +02:00
me0nline dacf01d5b5 Update out/linux_server/README.md with multi-tenant authority details 2026-09-25 16:22:28 +02:00
me0nline 432a488019 Update out/win_server/README.md with multi-tenant authority details 2026-09-25 16:22:23 +02:00
me0nline 384d096de2 Update README.md with multi-tenant and user management details 2026-09-25 16:22:03 +02:00
me0nline 85bae117b6 Update out/linux_server/README.md to document direct Hermes intelligence
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m38s
2026-09-25 16:16:33 +02:00
me0nline c8b309d0af Update out/win_server/README.md to document direct Hermes intelligence 2026-09-25 16:16:21 +02:00
me0nline 3b2c3e0fd7 Update README.md to describe direct Hermes anomaly reasoning 2026-09-25 16:16:07 +02:00
me0nline b7da54feb2 Update test_pipeline.py for direct Hermes anomaly verification 2026-09-25 16:15:21 +02:00
me0nline 2cd7a990b5 Update test_server.py to test direct Hermes anomaly verification 2026-09-25 16:15:11 +02:00
me0nline 2980ad2268 Remove 4-run rule and enable direct Hermes anomaly reasoning in Server.py 2026-09-25 16:14:31 +02:00
me0nline 95c33c1bf3 Update LOGAR-LICENSING submodule pointer for Portainer webhook URL
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 14s
2026-09-25 16:09:14 +02:00
me0nline 561ce2e037 Update LOGAR-LICENSING submodule pointer after removing 10.30.0.3
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 16s
2026-09-25 16:08:15 +02:00
me0nline e89ff9fa27 Remove 10.30.0.3 reference from linux_server README 2026-09-25 16:05:03 +02:00
me0nline 494fef07ba Remove 10.30.0.3 reference from win_server README 2026-09-25 16:04:53 +02:00
me0nline 4be0107329 Remove 10.30.0.3 registry reference from main README 2026-09-25 16:04:39 +02:00
me0nline 914db938c3 Update upstream authority section in linux_server README
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m39s
2026-09-25 16:03:14 +02:00
me0nline 506d3fc234 Update upstream authority section in win_server README 2026-09-25 16:02:33 +02:00
me0nline ff5caaf8c7 Document WebSocket protocol licensing and bootstrap .lic package flow in README 2026-09-25 16:02:17 +02:00
me0nline 63e8ff5492 Update LOGAR-LICENSING submodule pointer for WebSocket hub and login 2026-09-25 16:01:59 +02:00
me0nline 18e28e2797 Support WebSocket license sync and bootstrap .lic import in Server.py 2026-09-25 16:01:44 +02:00
me0nline 0e9e3f0f64 Add WebSocket license auto-enrollment and bootstrap .lic package support 2026-09-25 16:01:06 +02:00
me0nline ea8e98218c Add websockets dependency to compilation requirements 2026-09-25 15:57:02 +02:00
me0nline d9cfb06fad Update LOGAR-LICENSING submodule pointer for /webui and /health routes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-25 15:34:01 +02:00
me0nline 76812a1711 Update LOGAR-LICENSING submodule pointer for Gitea registry push
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 34s
2026-09-25 15:21:28 +02:00
me0nline 9584b2c229 Update container pipeline description in README 2026-09-25 15:21:17 +02:00
me0nline c9e9a5d267 Update LOGAR-LICENSING submodule pointer for registry password update
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 14s
2026-09-24 22:00:30 +02:00
me0nline 91aee0e591 Update LOGAR-LICENSING submodule pointer for crane insecure registry push
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 16s
2026-09-24 21:55:50 +02:00
me0nline 86ae3a11bc Update LOGAR-LICENSING submodule pointer after removing CodeChecker
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s
2026-09-24 21:54:07 +02:00
me0nline 76d53fbd92 Remove codechecker.yml from README repository structure 2026-09-24 21:53:58 +02:00
me0nline 39790d371b Remove CodeChecker workflow from main repository 2026-09-24 21:53:39 +02:00
me0nline 6300481349 Document codechecker.yml in main repository README
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 44s
CodeChecker Static Analysis / CodeChecker Static Analysis & Upload (push) Failing after 7s
2026-09-24 21:52:07 +02:00
me0nline 59029aa3d9 Update LOGAR-LICENSING submodule pointer with CodeChecker workflow 2026-09-24 21:51:43 +02:00
me0nline 431352ddce Add CodeChecker static analysis workflow to LOGAR 2026-09-24 21:51:33 +02:00
me0nline 52be1f23a6 Update registry image reference in linux_server README
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 34s
2026-09-24 18:30:52 +02:00
me0nline 353001b40f Update registry image reference in win_server README 2026-09-24 18:30:41 +02:00
me0nline 99743d9074 Document Kaniko compilation and 10.30.0.3:5000 registry in README.md 2026-09-24 18:30:28 +02:00
me0nline cbda93517b Update LOGAR-LICENSING submodule pointer for 10.30.0.3:5000 registry publication 2026-09-24 18:30:08 +02:00
me0nline 38b634b68f Update LOGAR-LICENSING submodule pointer with resilient deployment workflow
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 17s
2026-09-24 18:13:54 +02:00
me0nline 16539837c0 Update LOGAR-LICENSING submodule pointer with in-container test runner
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 19s
2026-09-24 18:08:06 +02:00
me0nline 7b9160af9c Update linux_server README with reverse proxy mTLS forwarding info
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 14s
2026-09-24 18:00:51 +02:00
me0nline f72bb06115 Update win_server README with reverse proxy mTLS forwarding info 2026-09-24 18:00:41 +02:00
me0nline 840daf0688 Document reverse proxy mTLS forwarding and Master CA in README.md 2026-09-24 18:00:02 +02:00
me0nline 947a786868 Update LOGAR-LICENSING submodule pointer for mTLS proxy forwarding and CA download 2026-09-24 17:59:38 +02:00
me0nline b457c7575f chore(submodule): update LOGAR-LICENSING pointer for port 8090
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 16s
2026-09-24 17:53:51 +02:00
me0nline ed9c4aa9d6 chore(submodule): update LOGAR-LICENSING with Portainer webhook configuration
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-24 17:50:05 +02:00
me0nline e4ea8f8deb docs(linux_client): note master authority high availability via Portainer GitOps
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-24 17:49:05 +02:00
me0nline 6399912995 docs(win_client): note master authority high availability via Portainer GitOps 2026-09-24 17:48:55 +02:00
me0nline e77437a042 docs(linux_server): add upstream authority Portainer GitOps deployment note 2026-09-24 17:48:37 +02:00
me0nline 1c2d46cef8 docs(win_server): add upstream authority Portainer GitOps deployment note 2026-09-24 17:48:20 +02:00
me0nline 9ff116fc4d docs: document container pipeline and Portainer GitOps stack for LOGAR-LICENSING 2026-09-24 17:48:07 +02:00
me0nline 983312573d chore(submodule): update LOGAR-LICENSING to include container pipeline and Portainer GitOps 2026-09-24 17:47:44 +02:00
me0nline bc2cc65571 chore(submodule): update LOGAR-LICENSING to include .lic package export and renewal seat expansion
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 15s
2026-09-24 17:41:13 +02:00
me0nline 4b74ea23af docs(linux_client): note dynamic seat expansion and master hub synchronization 2026-09-24 17:41:07 +02:00
me0nline 82eb10889d docs(win_client): note dynamic seat expansion and master hub synchronization 2026-09-24 17:38:42 +02:00
me0nline efa9ea7720 docs(linux_server): document .lic import, mTLS sync, and seat expansion 2026-09-24 17:38:27 +02:00
me0nline 25a36c8092 docs(win_server): document .lic import, mTLS sync, and seat expansion 2026-09-24 17:38:14 +02:00
me0nline 4171649bd3 docs: document activated .lic packages, mTLS license sync, and renewal seat expansion 2026-09-24 17:37:59 +02:00
me0nline 5670965e9d test(licensing): add tests for .lic package import, upstream CA persistence, and seat expansion 2026-09-24 17:37:45 +02:00
me0nline 032a5a61b4 fix(server): specify request: Request type annotation for FastAPI sync endpoint 2026-09-24 17:36:56 +02:00
me0nline b1327a4336 feat(server): support activated .lic import, mTLS sync for seat expansion, and WebUI controls 2026-09-24 17:36:33 +02:00
me0nline 6332dff670 feat(enrollment): add import_license_package supporting .lic files and seat renewal expansion 2026-09-24 17:32:13 +02:00
me0nline 8f21df03b4 docs: reference LOGAR-LICENSING submodule in root README
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 14s
2026-09-24 17:18:20 +02:00
me0nline 4943f5be62 fix(licensing): use importlib loader in src/License_Authority.py proxy to prevent circular imports 2026-09-24 17:18:05 +02:00
me0nline 5a303808a3 refactor(licensing): delegate src/License_Authority.py to LOGAR-LICENSING submodule 2026-09-24 17:17:35 +02:00
me0nline dc1617a8f1 feat(submodule): add LOGAR-LICENSING as a git submodule 2026-09-24 17:17:18 +02:00
37 changed files with 4938 additions and 3064 deletions
+58
View File
@@ -0,0 +1,58 @@
name: Build and Push LOGAR Runner
on:
workflow_dispatch:
release:
types: [published]
jobs:
build-and-push:
name: Build & Push Runner Image
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Ensure Docker CLI is available
run: |
if ! command -v docker >/dev/null 2>&1; then
echo "Docker CLI not found. Downloading static Docker CLI binary..."
curl -fSL --connect-timeout 15 --max-time 120 --retry 3 https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz -o /tmp/docker.tgz
tar -xzf /tmp/docker.tgz -C /usr/local/bin --strip-components=1 docker/docker
chmod +x /usr/local/bin/docker
rm -f /tmp/docker.tgz
fi
docker version
- name: Log in to Gitea Container Registry
run: |
REGISTRY="gitea.eibl.tech"
USER="${{ secrets.GITEAUSER || secrets.PACKAGESUSER || github.actor }}"
PASS="${{ secrets.GITEATOKEN || secrets.PACKAGESTOKEN || secrets.TAG_TOKEN || github.token }}"
echo "Logging in to ${REGISTRY} as ${USER}..."
echo "${PASS}" | docker login "${REGISTRY}" -u "${USER}" --password-stdin
- name: Build Docker Image
env:
DOCKER_BUILDKIT: "0"
run: |
IMAGE_TAG="gitea.eibl.tech/me0nline/logar-runner:latest"
echo "Building ${IMAGE_TAG}..."
docker build \
-t "${IMAGE_TAG}" \
-f runner/Dockerfile \
runner/
- name: Push to Gitea Container Registry
run: |
IMAGE_TAG="gitea.eibl.tech/me0nline/logar-runner:latest"
echo "Pushing ${IMAGE_TAG} to Gitea Container Registry..."
docker push "${IMAGE_TAG}"
echo "Successfully pushed ${IMAGE_TAG} to Gitea Container Registry!"
- name: Trigger Portainer redeploy via webhook
run: |
echo "Notifying Portainer to redeploy the runner stack..."
curl -s -X POST \
"https://portainer.eibl.tech/api/stacks/webhooks/6e67c0b7-1c5d-4bb2-b8b8-4a6ec66d3b87"
echo "✓ Portainer webhook triggered"
+11 -12
View File
@@ -15,19 +15,18 @@ jobs:
test: test:
name: Run Component Tests & Pipeline Verification name: Run Component Tests & Pipeline Verification
if: "!startsWith(github.ref, 'refs/tags/')" if: "!startsWith(github.ref, 'refs/tags/')"
runs-on: ubuntu-latest runs-on: logar
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4 uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install System Dependencies & Python - name: Install Dependencies
run: | run: |
if command -v apt-get >/dev/null 2>&1; then python3 compilation/ensure_pip.py
apt-get update -y python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
apt-get install -y python3 python3-pip python3-venv curl python3 -m pip install -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install -r compilation/requirements.txt
fi
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
pip3 install -r compilation/requirements.txt --break-system-packages || pip3 install -r compilation/requirements.txt
- name: Verify Python Syntax - name: Verify Python Syntax
run: | run: |
@@ -35,7 +34,7 @@ jobs:
- name: Run Component Unit Tests - name: Run Component Unit Tests
run: | run: |
python3 -m unittest discover -s tests -v PYTHONWARNINGS="ignore" python3 -m unittest discover -s tests -v
- name: Run End-to-End Pipeline Integration Test - name: Run End-to-End Pipeline Integration Test
run: | run: |
@@ -50,10 +49,10 @@ jobs:
SERVER_PID=$! SERVER_PID=$!
echo "[*] Server launched in background with PID $SERVER_PID" echo "[*] Server launched in background with PID $SERVER_PID"
# 3. Poll Hermes health / report endpoint until server is listening # 3. Poll /health endpoint until server is listening
READY=0 READY=0
for i in $(seq 1 20); do for i in $(seq 1 20); do
if curl -s http://127.0.0.1:8443/api/hermes/report >/dev/null 2>&1; then if python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8443/health', timeout=1)" >/dev/null 2>&1; then
echo "[+] LOGAR Server is ready after ${i}s." echo "[+] LOGAR Server is ready after ${i}s."
READY=1 READY=1
break break
+50 -98
View File
@@ -1,38 +1,72 @@
name: Release Binaries & Installers name: Release Binaries & Packages
on: on:
push: push:
tags: tags:
- 'v*' - 'v*'
release:
types: [published]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: 'Release tag to publish assets to (default: v2.0.1)' description: 'Release tag to publish assets to (default: v0.3.1)'
required: false required: false
default: 'v2.0.1' default: 'v0.3.1'
jobs: jobs:
release-linux: release:
name: Build & Release Linux Binaries name: Build & Release Standalone Binaries (Linux & Windows Cross-Compilation)
runs-on: ubuntu-latest runs-on: logar
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4 uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Python and Build Dependencies - name: Install System and Cross-Compilation Build Dependencies
run: | run: |
if command -v apt-get >/dev/null 2>&1; then echo "=== SYSTEM DIAGNOSTICS ==="
apt-get update -y uname -a || true
apt-get install -y python3 python3-pip python3-venv binutils zip python3 --version || true
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi fi
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true if command -v apt-get >/dev/null 2>&1; then
pip3 install pyinstaller -r compilation/requirements.txt --break-system-packages || pip3 install pyinstaller -r compilation/requirements.txt 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
- name: Compile Standalone Linux Binaries - name: Compile Standalone Binaries (Linux & Windows)
run: | run: |
python3 compilation/package_dist.py --target linux python3 compilation/package_dist.py --target all
- name: Publish Linux Release Assets - name: Publish Unified Release Assets
env: env:
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }} GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
GITEA_SERVER_URL: ${{ github.server_url }} GITEA_SERVER_URL: ${{ github.server_url }}
@@ -40,85 +74,3 @@ jobs:
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }} GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
run: | run: |
python3 compilation/upload_release.py --skip-build python3 compilation/upload_release.py --skip-build
release-windows:
name: Build & Release Windows Binaries & Installers
# Note: Requires a registered Gitea Act Runner with label 'windows-latest'
runs-on: windows-latest
steps:
- name: Checkout Repository
shell: powershell
run: |
$server = "${{ github.server_url }}"
$token = "${{ secrets.TAG_TOKEN || github.token }}"
$repo = "${{ github.repository }}"
$cleanUrl = $server -replace "^https?://", ""
$authUrl = "https://${token}@${cleanUrl}/${repo}.git"
if (-not (Test-Path ".git")) {
git init
git remote add origin $authUrl
} else {
git remote set-url origin $authUrl
}
git fetch --depth 1 origin "${{ github.sha }}"
git checkout -f FETCH_HEAD
- name: Install Dependencies
shell: powershell
run: |
$py = "python"
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
if (Get-Command "py" -ErrorAction SilentlyContinue) {
$py = "py -3.12"
}
}
& $py -m pip install --upgrade pip
& $py -m pip install pyinstaller cryptography -r compilation/requirements.txt
- name: Compile Standalone Windows Binaries
shell: powershell
run: |
$py = "python"
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
if (Get-Command "py" -ErrorAction SilentlyContinue) {
$py = "py -3.12"
}
}
& $py compilation/package_dist.py --target windows
- name: Compile Inno Setup Installers
shell: powershell
run: |
$iscc = $null
if (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe") {
$iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
} elseif (Test-Path "C:\Program Files\Inno Setup 6\ISCC.exe") {
$iscc = "C:\Program Files\Inno Setup 6\ISCC.exe"
} elseif (Get-Command "ISCC.exe" -ErrorAction SilentlyContinue) {
$iscc = "ISCC.exe"
}
if ($iscc) {
Write-Host "[*] Compiling Windows Inno Setup installers using $iscc..."
& $iscc compilation/installer_client.iss
& $iscc compilation/installer_server.iss
} else {
Write-Host "[!] Inno Setup compiler (ISCC.exe) not found on runner host. Skipping installer compilation."
}
- name: Publish Windows Release Assets
shell: powershell
env:
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
run: |
$py = "python"
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
if (Get-Command "py" -ErrorAction SilentlyContinue) {
$py = "py -3.12"
}
}
& $py compilation/upload_release.py --skip-build
+3
View File
@@ -29,6 +29,9 @@ server_config.json
client_config.json client_config.json
test_*.json test_*.json
test_*.db test_*.db
.env
*.env
runner/.env
# IDE & OS files # IDE & OS files
.vscode/ .vscode/
+3
View File
@@ -0,0 +1,3 @@
[submodule "LOGAR-LICENSING"]
path = LOGAR-LICENSING
url = https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git
+1
Submodule LOGAR-LICENSING added at 7bdb7300ec
+114 -348
View File
@@ -1,404 +1,170 @@
# LOGAR: Edge-Thin Log Analysis & Temporal Verification System # LOGAR: Fleet Log Aggregation & Centralized Provisioning System
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
**LOGAR** is an enterprise log aggregation, verification, and anomaly detection architecture designed for heterogeneous server fleets (Windows & Linux). It combines lightweight zero-state edge forwarders with a centralized cloud hub that applies mutual TLS 1.3 (**mTLS**) authentication, dynamic PKI licensing and quota management, temporal persistence tracking across 12-hour evaluation windows, an automated 4-run rule to filter transient warnings, and immediate pass-through for critical errors before reporting verified anomalies to **Hermes**. [![Version](https://img.shields.io/badge/version-0.3.1-blue.svg)](RELEASE_NOTES.md)
[![Tests](https://img.shields.io/badge/tests-35%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)
**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, and complete REST API references are maintained on the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**.
--- ---
## Table of Contents ## Key Capabilities
1. [Core Philosophy](#core-philosophy)
2. [Architecture & Data Flow](#architecture--data-flow) - **Zero-State Edge Forwarders**: Windows (`Win_Client`) and Linux (`Linux_Client`) agents maintain zero local databases, zero cache files, and minimal memory footprint.
3. [mTLS Security, Dynamic PKI & Licensing](#mtls-security-dynamic-pki--licensing) - **Mutual TLS 1.3 (`:9443`)**: Enforces bidirectional cryptographic identity verification (`ssl.CERT_REQUIRED`) for all streaming log frames.
4. [Cloud-Side Temporal Persistence & 4-Run Rule](#cloud-side-temporal-persistence--4-run-rule) - **Hierarchical PKI & Seat Licensing**: Subordinate CAs automatically enroll and sync seat quotas with the Master Authority (`logar.eibl.tech`) over WebSockets.
5. [Agentic Hermes & Client Management API](#agentic-hermes--client-management-api) - **12-Hour Temporal Persistence**: Evaluates anomalies over a rolling 12-hour temporal window, tracking occurrence velocity and first/last seen metrics.
6. [Dynamic Machine & Domain Identification](#dynamic-machine--domain-identification) - **Direct Hermes AI Provisioning (`:8443`)**: Exposes verified anomalous telemetry directly to Hermes AI via `/api/hermes/report` without heuristic bottlenecks.
7. [Automated Service Installers (Linux & Windows)](#automated-service-installers-linux--windows) - **WebUI Dashboard**: Authenticated, 2-tab management console for live anomaly telemetry, client seat leasing, and Sub-CA lifecycle management.
8. [Repository & Shippables Structure](#repository--shippables-structure)
9. [Getting Started & Deployment](#getting-started--deployment)
10. [Running Tests](#running-tests)
11. [Automated Releases via Gitea Actions](#automated-releases-via-gitea-actions)
--- ---
## Core Philosophy ## System Architecture
### 1. Edge Thinness & Zero State
Site agents running on Windows and Linux act strictly as lightweight forwarders:
- **No Local Database**: Clients maintain zero local SQLite or heavy cache files.
- **Source-Level Filtering**: Agents stream candidate entries from informational events up to errors (`INFO`, `WARNING`, `ERROR`, `CRITICAL`), stripping verbose debugging noise (`DEBUG`, trace entries) and skipping events older than 24 hours.
- **Transport Security (mTLS 1.3)**: Logs are streamed directly over mutual TLS 1.3 sockets with hardware-bound / machine-unique client certificates.
- **Zero Configuration Overhead**: Clients auto-bootstrap certificate enrollment on first run if configured with an enrollment secret.
### 2. Cloud-Side Temporal Persistence & Severity Routing
The central Python hub handles state and verification:
- State tracking is managed centrally in SQLite (`logar_state.db`).
- Candidate issues are evaluated over a **12-hour temporal evaluation window**.
- **Warning Persistence (4-Run Rule)**: `WARNING` issues must persist across **at least 4 consecutive runs / cycles** within the 12-hour window to be confirmed as genuine anomalies, automatically filtering transient infrastructure blips.
- **Immediate Error Pass**: Critical errors (`ERROR`, `CRITICAL`, `FATAL`) bypass the 4-run threshold and are promoted immediately to `VERIFIED` on their first occurrence.
### 3. Agentic Integration with Hermes
Instead of engineers manually sifting through logs, **Hermes** ingests pre-filtered anomalies directly from the cloud hub (`GET /api/hermes/report`), treating verified errors and 4-run validated warnings as actionable system artifacts to trigger precise notifications and remediations.
---
## Architecture & Data Flow
```mermaid ```mermaid
graph TB graph TB
subgraph Edge Nodes [Lightweight Edge Forwarders] subgraph Authority [Master License Authority - logar.eibl.tech]
W[Win_Client.exe / Win_Client.py<br/>Windows Event Log Ingestion] M_CA[Master Root CA]
L[Linux_Client.bin / Linux_Client.py<br/>systemd journalctl -p warning] AUTH_WS[WebSocket Licensing Hub]
end end
subgraph Enrollment [Dynamic PKI & Licensing] subgraph ServerHub [LOGAR Server Hub]
ENR[POST /api/client/enroll<br/>License Quota & Secret Validation] SUB_CA[Subordinate CA]
CA[Internal Root CA<br/>Signs RSA-2048 Client Cert] SEAT_MGR[Dynamic Seat Manager]
MTLS_SRV[mTLS 1.3 Ingestion Server<br/>Port 9443]
TEMP_ENG[12h Temporal Persistence Engine]
PROV_API[Hermes Provisioning & WebUI API<br/>Port 8443]
SUB_CA --> SEAT_MGR
SEAT_MGR --> MTLS_SRV
MTLS_SRV --> TEMP_ENG
TEMP_ENG --> PROV_API
end end
subgraph Transport [mTLS 1.3 Security Layer] subgraph EdgeNodes [Heterogeneous Edge Forwarders]
MTLS[Mutual TLS 1.3 Handshake<br/>Port 9443 - Client Cert Required] WIN[Win_Client.exe<br/>Windows Event Logs]
AUTH[Extract Client CN & Fingerprint<br/>Validate Active License in SQLite] LIN[Linux_Client.bin<br/>systemd journalctl]
end end
subgraph Hub [LOGAR Server Hub] subgraph Downstream [Hermes AI Layer]
INGEST[Length-Prefixed Frame Ingestion] HERMES[Hermes AI Engine]
DB[(SQLite Persistence<br/>active_issues, clients, license_config)]
RULE{12h Window &<br/>4-Run Rule}
end end
subgraph Downstream [Hermes Agent & Monitoring] M_CA -->|Signs Sub-CA Cert| SUB_CA
API[FastAPI Reporting & Management<br/>Port 8443] AUTH_WS <==>|Live WebSocket Sync| SUB_CA
HERMES[Hermes Agent<br/>GET /api/hermes/report]
end WIN -->|1. Auto-Enrollment| SUB_CA
LIN -->|1. Auto-Enrollment| SUB_CA
W -->|Auto-Enrollment| ENR WIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
L -->|Auto-Enrollment| ENR LIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
ENR --> CA PROV_API ==>|3. GET /api/hermes/report| HERMES
CA -->|ca.crt, client.crt, client.key| W
CA -->|ca.crt, client.crt, client.key| L
W -->|mTLS Stream| MTLS
L -->|mTLS Stream| MTLS
MTLS --> AUTH
AUTH --> INGEST
INGEST --> RULE
RULE --> DB
DB --> API
API --> HERMES
``` ```
--- ---
## mTLS Security, Dynamic PKI & Fleet Licensing ## Quick Start & Automated Installers
### 1. TLS 1.3 Mutual Authentication (mTLS) LOGAR provides automated production service installers for both Linux (`systemd`) and Windows (`services.msc` via NSSM):
- **Port 9443**: Ingestion occurs exclusively over TLS 1.3 sockets with `ssl.CERT_REQUIRED`.
- Both the hub and edge clients verify each other's certificates:
- Client verifies server certificate against `ca.crt`.
- Server verifies client certificate against the Subordinate/Root CA.
- **Client CN Identification & Revocation**: In the TLS handshake, the server extracts the `commonName` attribute (`client_id`), validates that the client is marked `active` in the `clients` table, updates the `last_seen` timestamp, and drops unregistered or revoked certificates immediately.
### 2. Master License Authority (`logar.eibl.tech`) & Seat-Bearing Subordinate CA ### 1. Server Hub Installation
- **Master Root Authority**: Standalone PKI service ([`src/License_Authority.py`](file:///u:/max/Desktop/LOGAR/src/License_Authority.py)) maintained at `logar.eibl.tech`.
- **Custom Enterprise X.509 OIDs**:
- `OID_LOGAR_SEATS (1.3.6.1.4.1.59999.1.1)`: Embeds the exact licensed seat quota directly inside the signed Subordinate CA certificate.
- `OID_LOGAR_LICENSE_ID (1.3.6.1.4.1.59999.1.2)`: Uniquely tracks enterprise license entitlement.
- `OID_LOGAR_ISSUER_URL (1.3.6.1.4.1.59999.1.3)`: Embedded endpoint coordinate (`https://logar.eibl.tech`).
- **Exportable `.lic` Request Files**:
- The LOGAR server generates its Subordinate CA private key (`certs/ca.key`), Certificate Signing Request (`certs/ca.csr`), and packages them into an exportable `logar_server.lic` file containing the CSR, server fingerprint, and license authority coordinate.
- Exportable directly from the WebUI or via CLI (`python src/Server.py --export-lic server.lic`).
- **One-Time Pass & Zero-Touch mTLS Renewals**:
- The signed CA certificate only needs to be imported **once** into the LOGAR Server (via WebUI or CLI `--import-ca`).
- Once imported, the server hub's background watchdog monitors `ca.crt` expiration (< 30 days).
- Renewals are performed **automatically over mTLS**: the LOGAR Server authenticates directly against `https://logar.eibl.tech/api/license/renew` using its existing active certificate and key, receives the renewed Subordinate CA certificate preserving its seat quota, and reloads its in-flight `ssl.SSLContext` without dropping client connections or restarting services.
### 3. Integrated Fleet Licensing & WebUI Dashboard - **Linux (Automated systemd Installer)**:
- **WebUI URL**: Accessible at `http://<server_host>:8443/webui` (or `/`).
- **Visual Seat Quota Meter**: Real-time progress bar displaying active forwarders vs. allocated seat limit.
- **Licensing Actions**:
- 1-click **Export .lic Request** file download.
- Interactive **View & Copy CA CSR** modal.
- Drag-and-drop or paste **Import Signed CA Certificate** modal.
- **Renew via mTLS Now** button for immediate upstream renewal.
- **Seat Management & Revocation Table**:
- Lists all connected Windows and Linux edge forwarders.
- **Revoke Seat**: Instantly drops and blocks the client at the TLS socket handshake layer.
- **Reactivate Seat**: Restores the client to active status if within seat quota limits.
### 4. Cloud-Side In-Flight Certificate Watchdog
- **Continuous Hub PKI Watchdog**:
- The server hub runs a continuous background watchdog coroutine (`cert_validity_watchdog`, running every 12 hours).
- Automatically renews Subordinate CA certificates over mTLS with `logar.eibl.tech` and Server TLS certificates in-flight, dynamically reloading the active `ssl.SSLContext`.
- **Client Proactive Check & Reactive Self-Healing**:
- **Proactive Renewal**: Edge clients inspect `client.crt` before every run cycle. If expiring within 30 days, they contact `/api/client/enroll` to renew.
- **Reactive Self-Healing**: If the server CA rotates or a handshake error occurs, edge clients catch the exception, re-bootstrap enrollment against the hub, and re-establish streaming cleanly.
---
## Cloud-Side Temporal Persistence & 4-Run Rule
Incoming candidate logs are tracked in SQLite table `active_issues`:
- **Issue Fingerprint**: Formatted as `{site_name}:{server}:{signature}`.
- **12-Hour Evaluation Window**:
- The hub compares `(now - last_seen)`.
- If more than 12 hours have elapsed since the issue was last recorded, the previous window expires and the cycle resets to `run_count = 1`.
- **4-Run Rule for Warnings**:
- The 4-run persistence threshold applies to `WARNING` (and `INFO`) events to eliminate transient operational noise.
- Each distinct run batch increments `run_count`.
- Warnings with `run_count < 4` are marked as `TRANSIENT` and excluded from Hermes reports.
- When `run_count >= 4` within the active 12-hour window, the warning transitions to `VERIFIED`.
- **Immediate Verification for Errors**:
- High-severity events (`ERROR`, `CRITICAL`, `FATAL`) **always pass immediately**.
- On their very first ingestion (`run_count = 1`), errors are promoted directly to `VERIFIED` and surfaced to Hermes without waiting for 4 runs.
---
## Agentic Hermes & Client Management API
The server hub exposes a management and reporting REST API (default port `8443`):
### `POST /api/client/enroll`
Client enrollment endpoint:
- **Request**:
```json
{
"client_id": "web-worker-01.corp.internal",
"hostname": "web-worker-01",
"os": "linux",
"enrollment_secret": "<SECRET>"
}
```
- **Response**:
```json
{
"ca_cert": "-----BEGIN CERTIFICATE-----\n...",
"client_cert": "-----BEGIN CERTIFICATE-----\n...",
"client_key": "-----BEGIN RSA PRIVATE KEY-----\n..."
}
```
### `GET /api/clients`
Returns seat quota status and registered client telemetry:
```json
{
"active_seats": 2,
"max_seats": 10,
"clients": [
{
"client_id": "web-worker-01.corp.internal",
"hostname": "web-worker-01",
"os_type": "linux",
"cert_fingerprint": "7D5B660B...",
"status": "active",
"first_seen": "2026-09-04 18:00:00",
"last_seen": "2026-09-04 19:15:00"
}
]
}
```
### `GET /api/hermes/report`
Returns all **verified anomalies** (immediate critical errors and warnings verified after 4 consecutive runs within the 12-hour window):
```json
[
{
"fingerprint": "corp.internal:web-worker-01.corp.internal:PostgresPoolExhausted",
"site": "corp.internal",
"server": "web-worker-01.corp.internal",
"signature": "PostgresPoolExhausted",
"severity": "WARNING",
"message": "Connection pool saturated (>95%) across 4 runs",
"os_type": "linux",
"first_seen": "2026-09-04T07:00:00+00:00",
"last_seen": "2026-09-04T19:00:00+00:00",
"consecutive_runs": 4,
"evaluation_window": "12h",
"verified": true,
"status": "VERIFIED"
}
]
```
### `GET /api/hermes/all`
Diagnostic endpoint listing all candidate issues (`TRANSIENT` and `VERIFIED`).
### `GET /health`
Returns hub health, encryption fingerprint, listener ports, and mTLS status.
---
## Dynamic Machine & Domain Identification
Client configurations intentionally contain **no hardcoded machine name or site name**. Both forwarders dynamically identify their host and domain at runtime via `get_machine_identifier()`:
1. **Fully Qualified Domain Name (FQDN)**: Checked via `socket.getfqdn()`.
2. **OS-Specific Domain Discovery**:
- **Windows**: Checks Active Directory environment variables (`USERDNSDOMAIN`, `USERDOMAIN`).
- **Linux**: Parses `/etc/resolv.conf` `domain` and `search` directives.
3. **Reverse DNS Lookup**: Resolves canonical hostname via `socket.gethostbyaddr`.
4. **Fallback**: Local hostname `socket.gethostname()`.
The server automatically infers site attribution from domain qualifiers (e.g. `node01.corp.internal` $\rightarrow$ site `corp.internal`).
---
## Automated Service Installers (Linux & Windows)
LOGAR provides production-grade installation scripts and installer builders for automated service deployment:
### 1. Linux Service Installers
- **Client Installer (`compilation/install_linux_client.sh`)**:
- Non-interactive script deploying to `/opt/logar-client`.
- Automatically queries `/etc/machine-id` and enrolls with the hub via `curl`.
- Installs and enables `logar-client.service` systemd unit.
```bash
sudo ./compilation/install_linux_client.sh "http://hub.example.com:8443" "<ENROLLMENT_SECRET>"
```
- **Server Installer (`compilation/install_linux_server.sh`)**:
- Deploys server to `/opt/logar-server`.
- Configures logging and installs `logar-server.service` with `LimitNOFILE=65536`.
```bash ```bash
sudo ./compilation/install_linux_server.sh sudo ./compilation/install_linux_server.sh
``` ```
*Deploys standalone `Server.bin` to `/opt/logar-server`, initializes PKI, and enables `logar-server.service`.*
### 2. Windows Inno Setup Installers - **Windows (Inno Setup / NSIS Installer)**:
- Built using **Inno Setup 6** and bundled with **NSSM** (`compilation/nssm.exe`): Download and execute `LOGAR-Server-Setup.exe` from [Releases](https://gitea.eibl.tech/me0nline/LOGAR/releases). Installs `Server.exe` into `C:\Program Files\LOGAR-Server` and registers `LOGAR_Server` Windows service.
- **Client Setup (`compilation/installer_client.iss`)**: Compiles `LOGAR-Client-Setup.exe`. Installs `Win_Client.exe` into `{autopf}\LOGAR`, sets up `LOGAR_Client` service via NSSM with stdout/stderr redirection to `{commonappdata}\LOGAR\client.log`, and starts the service. Clean uninstallation stops and removes the service.
- **Server Setup (`compilation/installer_server.iss`)**: Compiles `LOGAR-Server-Setup.exe`. Installs `Server.exe` and sets up `LOGAR_Server` Windows service via NSSM. #### Default WebUI Management Credentials
Once installed or executed, access the web console:
- **URL**: `http://<server-ip>:8443/webui`
- **Default Username**: `admin`
- **Default Password**: `admin_logar_2026!` *(Configurable via `LOGAR_ADMIN_USER` / `LOGAR_ADMIN_PASS` or in the WebUI user settings)*
### 2. Edge Forwarder Installation
- **Linux Client (1-Line Non-Interactive Installer)**:
```bash
sudo ./compilation/install_linux_client.sh "http://<HUB_HOST>:8443" "<ENROLLMENT_SECRET>"
```
*Auto-enrolls mTLS certificates to `/etc/logar/certs` (`chmod 700`), generates configuration, and activates `logar-client.service`.*
- **Windows Client (Inno Setup Installer)**:
1. Download and run `LOGAR-Client-Setup.exe` (supports silent install `/VERYSILENT`).
2. Place `client_config.json` (exported from Server WebUI) into `C:\Program Files\LOGAR\client_config.json`.
3. The `LOGAR_Client` service starts and auto-enrolls immediately.
*(Alternatively, standalone binaries `./Server.bin` / `./Linux_Client.bin` / `.\Server.exe` / `.\Win_Client.exe` can be executed directly without service installation).*
--- ---
## Repository & Shippables Structure ## Repository Structure
``` ```
LOGAR/ LOGAR/
├── .gitea/ ├── .gitea/workflows/ # CI/CD pipelines (ci.yml, release.yml)
│ └── workflows/ ├── compilation/ # Build scripts, Linux installers & Inno Setup scripts
│ ├── ci.yml # CI pipeline: syntax, 25 unit tests & mTLS pipeline test │ ├── install_linux_client.sh # Linux client systemd auto-installer
│ └── release.yml # Consolidated release workflow (Linux binaries & Windows installers) │ ├── install_linux_server.sh # Linux server systemd auto-installer
├── compilation/ # Packaging, installers, and release automation │ ├── package_dist.py # Standalone binary cross-compiler
│ ├── install_linux_client.sh # Automated Linux client systemd installation script │ └── upload_release.py # Gitea release asset publisher
│ ├── install_linux_server.sh # Automated Linux server systemd installation script ├── src/ # Application source code
│ ├── installer_client.iss # Inno Setup Windows Client installer script │ ├── Server.py # Central mTLS hub, temporal engine & REST API
│ ├── installer_server.iss # Inno Setup Windows Server installer script │ ├── server_enrollment.py # Sub-CA PKI and certificate generator
│ ├── nssm.exe # Official 64-bit NSSM service manager binary │ ├── Win_Client.py # Windows edge forwarder
│ ├── package_dist.py # Standalone binary compiler & packager │ └── Linux_Client.py # Linux edge forwarder
│ ├── requirements.txt # Unified project dependencies ├── tests/ # Automated test suites (35 unit tests + pipeline test)
│ └── upload_release.py # Gitea REST API release asset publisher ├── LOGAR-LICENSING/ # Standalone Master License Authority (Git submodule)
├── src/ # Core application source modules ├── RELEASE_NOTES.md # Version history and security changelog
│ ├── __init__.py └── README.md # Repository overview
│ ├── Server.py # Central mTLS server, temporal engine, and Hermes REST API
│ ├── server_enrollment.py # Dynamic PKI, Root CA, and client certificate generator
│ ├── Win_Client.py # Windows edge forwarder with auto-enrollment
│ └── Linux_Client.py # Linux edge forwarder with auto-enrollment
├── tests/ # Automated test suites
│ ├── test_linux_client.py # Linux client unit tests & mTLS certificate validation
│ ├── test_pipeline.py # End-to-end mTLS integration & 4-run verification test
│ ├── test_server.py # Server unit tests, PKI generation, and seat quota tests
│ └── test_win_client.py # Windows client unit tests & mTLS certificate validation
├── .gitignore # Ignores venv, caches, DBs, and private keys
├── README.md # Architecture and usage documentation
├── RELEASE_NOTES.md # Release history and changelog
├── server_config.sample.json # Reference server configuration
└── out/ # Component guides and sample configs
├── linux_server/
│ ├── README.md
│ └── server_config.sample.json
├── win_server/
│ ├── README.md
│ └── server_config.sample.json
├── linux_client/
│ ├── README.md
│ └── client_config.sample.json
└── win_client/
├── README.md
└── client_config.sample.json
``` ```
--- ---
## Getting Started & Deployment ## Ports & REST API Summary
### 1. Central Server Hub | Port | Protocol | Service | Description |
| :--- | :--- | :--- | :--- |
| **`9443`** | `TCP / TLS 1.3` | **mTLS Log Ingestion** | Mutual TLS socket listener for edge forwarders |
| **`8443`** | `HTTP / REST` | **Management & Hermes API** | WebUI, forwarder enrollment, seat management, and AI reports |
1. **Install dependencies**: ### Core Endpoints
```bash
pip install -r compilation/requirements.txt
```
2. **Start the server** (generates `server_config.json`, Root CA, and server certs on first run):
```bash
python src/Server.py
```
3. **Export a client configuration**:
```bash
python src/Server.py --create-client-config --server-host <SERVER_IP> --server-port 9443 --client-out client_config.json
```
### 2. Windows Client Deployment - **`POST /api/client/enroll`**: Forwarder auto-enrollment and proactive certificate renewal.
1. Download `LOGAR-Client-Setup.exe` from releases and run it, or place `Win_Client.exe` and `client_config.json` in `C:\Program Files\LOGAR`. - **`GET /api/clients`**: Returns active seat quota and registered edge forwarder telemetry *(Admin Auth)*.
2. On first run with `client_config.json`, `Win_Client.exe` automatically enrolls with the hub, receives its mTLS certificates, and establishes secure streaming. - **`GET /api/hermes/report`**: Structured JSON anomaly stream for Hermes AI reasoning.
- **`POST /api/license/sync`**: Triggers immediate WebSocket sync with `logar.eibl.tech` *(Admin Auth)*.
### 3. Linux Client Deployment - **`GET /health`**: Server health, encryption fingerprint, and mTLS status.
1. Run the automated installer:
```bash
sudo ./compilation/install_linux_client.sh "http://<HUB_HOST>:8443" "<ENROLLMENT_SECRET>"
```
2. The installer enrolls the client, configures `/etc/logar/certs`, and activates `logar-client.service`.
--- ---
## Running Tests ## Testing & Cross-Compilation
### 1. Component Unit Tests
```bash ```bash
# Run full unit test suite (35 tests)
python -m unittest discover -s tests -v python -m unittest discover -s tests -v
```
Runs all **25 unit tests**, covering:
- Dynamic Root CA generation and server TLS certificate issuance.
- Dynamic client certificate issuance with CN and authority key extensions.
- Enrollment secret authentication, seat limits, and certificate revocation.
- Proactive certificate validity checks, Root CA auto-renewal, and in-flight server SSLContext reload.
- Windows & Linux event log collection, deduplication, and mTLS certificate verification.
- 12-hour evaluation window and 4-run rule progression.
### 2. End-to-End Pipeline Integration Test # Run end-to-end mTLS pipeline integration test
```bash
# 1. Initialize test configuration
python src/Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
# 2. Launch server in background
python src/Server.py &
# 3. Run integration test
python tests/test_pipeline.py python tests/test_pipeline.py
# Cross-compile native Linux and Windows binaries on Linux runner
python compilation/package_dist.py --target all
``` ```
Tests client enrollment, secret rejection, mTLS TLS 1.3 socket handshake, warning 4-run rule promotion, immediate error promotion, and Hermes report output.
--- ---
## Releases & Binary Distribution ## Documentation & Deep Dives
Releases can be built and published through three complementary channels: For in-depth operational guides, lifecycle state machines, and PKI specifications, consult the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**:
### 1. Tag Push Automation (Gitea Actions) * [Architecture & Design Details](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Architecture-and-Design)
Pushing a release tag automatically triggers the build workflows: * [System State Machines & Subsystem Lifecycles](https://gitea.eibl.tech/me0nline/LOGAR/wiki/State-Machines-and-Lifecycles)
```bash * [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
git tag v2.0.1 * [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
git push origin v2.0.1 * [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)
The consolidated workflow **`release.yml`** defines two parallel jobs:
- **`release-linux`** (`ubuntu-latest`): Compiles standalone native ELF binaries (`Linux_Client.bin`, `Server.bin`) and checksums.
- **`release-windows`** (`windows-latest`): Compiles Windows executables (`Win_Client.exe`, `Server.exe`), builds Inno Setup installers, and publishes checksums (requires self-hosted Windows Act Runner).
### 2. Manual Workflow Dispatch (Gitea UI)
Workflows can be manually triggered on demand from the Gitea web interface:
1. Navigate to **Actions** $\rightarrow$ **Release Binaries & Installers** (`release.yml`).
2. Click **Run workflow**, set the release tag (defaults to `v2.0.1`), and run.
### 3. Native Local Windows Build & Direct Release Upload
For environments without a registered Windows CI runner, Windows executables can be built and published directly to Gitea releases:
```powershell
# 1. Build Windows binaries locally
python compilation/package_dist.py --target windows
# 2. Upload assets and release notes directly to the Gitea release
python compilation/upload_release.py --tag v2.0.1 --token <GITEA_TOKEN> --skip-build
```
+11 -58
View File
@@ -1,60 +1,13 @@
# LOGAR Release v2.0.1 # LOGAR Release v0.3.1
Maintenance and deployment release consolidating Gitea Actions release automation into a unified single workflow file, standardizing release dispatching across platforms, and bumping version definitions across server hub and Windows installers. ### 🛡️ Security Fixes & Defensive Hardening
- **Mandatory Admin Authentication**: Enforced strict administrator authentication across all fleet management endpoints (`/api/clients`, `/api/clients/approve`, `/api/clients/revoke`, `/api/clients/delete`, `/api/client/export-profile`) and license management APIs.
### Key Highlights & Changes in v2.0.1: - **Brute-Force Mitigation & Rate Limiting**: Implemented thread-safe sliding-window IP rate limiting on `/login` (10 attempts/min) and `/api/client/enroll` (15 attempts/min) with progressive delays and `HTTP 429` throttling.
- **Cookie Security Flags**: Session cookies now enforce `HttpOnly=True`, `SameSite="Lax"`, and dynamic `Secure=True` for HTTPS connections and reverse-proxy deployments (`X-Forwarded-Proto`).
- **Consolidated Single Release Automation Workflow (`.gitea/workflows/release.yml`)**: - **Cryptographic Random Secrets**: Server Hub and License Authority session secrets are now generated with 256-bit CSPRNG (`secrets.token_hex(32)`) on initialization.
- Unified separate platform release files into a single, cohesive workflow (`release.yml`) running parallel jobs (`release-linux` on `ubuntu-latest` and `release-windows` on `windows-latest`). - **License Renewal Validation**: Hardened `/api/license/renew` with explicit PEM structure and database validation, preventing unhandled exceptions.
- Standardized tag matching for Gitea Actions on `push: tags: ['v*']`.
- Added streamlined manual `workflow_dispatch` triggers with automated release tag defaulting (`v2.0.1`).
- Retired deprecated `release-linux.yml` and `release-windows.yml` files.
- **Installer & Engine Version Bump**:
- Updated FastAPI Hub engine version to `2.0.1` in `src/Server.py`.
- Bumped Inno Setup Windows Client Installer (`compilation/installer_client.iss`) `AppVersion` to `2.0.1`.
- Bumped Inno Setup Windows Server Installer (`compilation/installer_server.iss`) `AppVersion` to `2.0.1`.
- **Distribution & Release Documentation**:
- Updated root and distribution documentation across all 5 deployment targets to reflect the 25 passing unit tests and tripartite release options.
---
# LOGAR Release v2.0.0
Major architectural release introducing Mutual TLS 1.3 (mTLS) transport security, built-in dynamic PKI & license accounting, in-flight certificate validity monitoring and auto-renewal, and automated Windows and Linux service installers.
### Key Highlights & Changes in v2.0.0:
- **mTLS 1.3 Transport Security & Runtime Licensing**:
- Replaced legacy plain TCP sockets with mutual TLS 1.3 authentication (`ssl.CERT_REQUIRED`, TLS 1.3 minimum version).
- Hub dynamically validates incoming client Common Name (`client_id`) against active license seats in SQLite during the TLS handshake.
- Drops unauthorized, un-enrolled, or revoked clients at the transport layer before payload reading.
- **Dynamic Hub PKI Engine (`src/server_enrollment.py`)**:
- Automatically initializes an internal RSA-4096 Root CA (`ca.crt` / `ca.key`).
- Generates RSA-2048 Server TLS certificates with SANs for localhost, loopback, and server hostnames.
- Full OpenSSL 3.x and Python 3.12–3.14 compatibility via `SubjectKeyIdentifier` and `AuthorityKeyIdentifier` certificate extensions.
- Generates and signs client certificates on demand via `POST /api/client/enroll`.
- **In-Flight Certificate Validity Watchdog & Dynamic SSLContext Reloading**:
- Server hub runs a continuous background watchdog coroutine (`cert_validity_watchdog`, evaluated every 12 hours) alongside startup checks.
- Automatically checks Root CA and server TLS certificate expiration against a 30-day threshold.
- Generates renewed certificates on disk with timestamped backups (`.bak`), and reloads active `ssl.SSLContext` in memory dynamically without dropping socket listeners or restarting the background service.
- **Client Proactive Expiry Check & Reactive Self-Healing Auto-Renewal**:
- **Proactive**: Forwarders (`Win_Client.py` and `Linux_Client.py`) evaluate `client.crt` validity before each run, auto-renewing via `/api/client/enroll` if expiring within 30 days.
- **Reactive**: If the hub rotates its Root CA or a TLS verification error (`ssl.SSLError` / `SSLCertVerificationError`) occurs, clients automatically catch the error, re-enroll with the hub using their enrollment secret, and reconnect cleanly.
- **Database Schema & License Quota Accounting**:
- SQLite tables `license_config` (`max_seats`, `enrollment_secret`) and `clients` (`client_id`, `hostname`, `os_type`, `cert_fingerprint`, `status`, timestamps).
- Enforces seat limits on enrollment (`HTTP 403 License seat limit reached`) while allowing active registered nodes to re-enroll/renew indefinitely.
- Added `GET /api/clients` endpoint for license auditing and telemetry tracking.
- **Automated Service Installers**:
- **Windows**: Self-contained Inno Setup installers (`LOGAR-Client-Setup.exe` and `LOGAR-Server-Setup.exe`) bundling `nssm.exe` to register, configure, and start Windows services automatically.
- **Linux**: Automated installer scripts (`compilation/install_linux_client.sh` and `install_linux_server.sh`) deploying systemd service units with auto-restart policies.
- **CI/CD Release Workflows**:
- Windows workflow (`.gitea/workflows/release-windows.yml`) and Linux workflow (`.gitea/workflows/release-linux.yml`) automated to build native executables, installers, and upload release assets on tag push.
### ⚡ Operational Improvements & Modernization
- **FastAPI Lifespan Handlers**: Migrated legacy startup events to modern ASGI lifespan context managers.
- **OpenPGP Compression Preferences**: Aligned Server public key compression algorithms (`ZIP`, `ZLIB`, `BZ2`, `Uncompressed`) with client preferences.
- **Cross-Platform Compilation**: Unified automated packaging for standalone Windows and Linux binaries.
+41
View File
@@ -0,0 +1,41 @@
import sys
import subprocess
import urllib.request
def main():
try:
import pip # noqa: F401
print("[+] pip is already installed.")
return
except ImportError:
pass
try:
import ensurepip
ensurepip.bootstrap()
print("[+] Successfully bootstrapped pip via ensurepip.")
return
except Exception as e:
print(f"[*] ensurepip bootstrap skipped ({e}), downloading get-pip.py...")
ver = f"{sys.version_info.major}.{sys.version_info.minor}"
url = f"https://bootstrap.pypa.io/pip/{ver}/get-pip.py" if sys.version_info < (3, 10) else "https://bootstrap.pypa.io/get-pip.py"
dest = "/tmp/get-pip.py" if sys.platform != "win32" else "get-pip.py"
print(f"[*] Downloading get-pip.py from {url} for Python {ver}...")
try:
urllib.request.urlretrieve(url, dest)
cmd = [sys.executable, dest]
if sys.version_info >= (3, 11):
cmd.append("--break-system-packages")
res = subprocess.run(cmd)
if res.returncode == 0:
print("[+] Successfully installed pip via get-pip.py")
else:
# Retry without break-system-packages in case old pip version rejected it
subprocess.run([sys.executable, dest])
except Exception as ex:
print(f"[!] Warning during pip bootstrap: {ex}")
if __name__ == "__main__":
main()
+1
View File
@@ -9,6 +9,7 @@ CONFIG_DIR="/etc/logar"
echo "[+] Installing LOGAR Client..." echo "[+] Installing LOGAR Client..."
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs" mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
chmod 700 "${CONFIG_DIR}/certs"
if [ -f "dist/Linux_Client.bin" ]; then if [ -f "dist/Linux_Client.bin" ]; then
cp dist/Linux_Client.bin "${INSTALL_DIR}/Linux_Client" cp dist/Linux_Client.bin "${INSTALL_DIR}/Linux_Client"
+25 -1
View File
@@ -4,7 +4,31 @@ set -euo pipefail
INSTALL_DIR="/opt/logar-server" INSTALL_DIR="/opt/logar-server"
CONFIG_DIR="/etc/logar" CONFIG_DIR="/etc/logar"
echo "[+] Installing LOGAR Server..." # Parse command line options
while [[ $# -gt 0 ]]; do
case "$1" in
--config-dir|--data-dir)
CONFIG_DIR="$2"
shift 2
;;
*)
shift
;;
esac
done
echo "============================================================"
echo " LOGAR Server Enterprise Linux Installer"
echo "============================================================"
# Interactive configuration if running in a terminal without CLI arguments
if [ -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
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar" mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
if [ -f "dist/Server.bin" ]; then if [ -f "dist/Server.bin" ]; then
+74 -4
View File
@@ -1,27 +1,97 @@
[Setup] [Setup]
AppName=LOGAR Client AppName=LOGAR Client
AppVersion=2.0.1 AppVersion=0.3.1
AppPublisher=LOGAR Security Systems
AppPublisherURL=https://logar.eibl.tech
AppSupportURL=https://logar.eibl.tech
AppUpdatesURL=https://logar.eibl.tech
DefaultDirName={autopf}\LOGAR DefaultDirName={autopf}\LOGAR
OutputDir=..\dist OutputDir=..\dist
OutputBaseFilename=LOGAR-Client-Setup OutputBaseFilename=LOGAR-Client-Setup
PrivilegesRequired=admin PrivilegesRequired=admin
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern
VersionInfoCompany=LOGAR Security Systems
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
VersionInfoVersion=0.3.1.0
VersionInfoProductName=LOGAR Client
VersionInfoProductVersion=0.3.1
[Files] [Files]
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs] [Dirs]
Name: "{commonappdata}\LOGAR"; Permissions: users-modify Name: "{code:GetDataDir}"; Permissions: users-modify
[Run] [Run]
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Client ""{app}\Win_Client.exe"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Client ""{app}\Win_Client.exe"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppDirectory ""{app}"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppDirectory ""{app}"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStdout ""{commonappdata}\LOGAR\client.log"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStdout ""{code:GetDataDir}\client.log"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStderr ""{commonappdata}\LOGAR\client_err.log"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStderr ""{code:GetDataDir}\client_err.log"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppParameters ""--config """"{code:GetDataDir}\client_config.json"""""""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Client"; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Client"; Flags: runhidden
[UninstallRun] [UninstallRun]
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Client"; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Client"; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Client confirm"; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Client confirm"; Flags: runhidden
[UninstallDelete]
Type: filesandordirs; Name: "{app}\certs"
Type: files; Name: "{app}\client_config.json"
Type: files; Name: "{app}\client_state.json"
Type: files; Name: "{app}\*.log"
Type: filesandordirs; Name: "{app}\runtime"
Type: filesandordirs; Name: "{app}"
Type: filesandordirs; Name: "{commonappdata}\LOGAR"
[Code]
var
DataDirPage: TInputDirWizardPage;
procedure InitializeWizard;
begin
DataDirPage := CreateInputDirPage(
wpSelectDir,
'Data and Configuration Location',
'Where should LOGAR Client store its state tracking and certificates?',
'Select the folder where LOGAR Client should store state tracking (client_state.json), configuration (client_config.json), and mTLS certificates.'#13#10#13#10 +
'By default, this is stored in Common AppData (C:\ProgramData\LOGAR), which is accessible to background Windows services.',
False,
''
);
DataDirPage.Add('Data Directory:');
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
end;
function GetDataDir(Param: String): String;
begin
Result := DataDirPage.Values[0];
if Result = '' then
Result := ExpandConstant('{commonappdata}\LOGAR');
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
CustomDataDir: String;
begin
if CurUninstallStep = usPostUninstall then
begin
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', CustomDataDir) then
begin
if (CustomDataDir <> '') and DirExists(CustomDataDir) then
DelTree(CustomDataDir, True, True, True);
RegDeleteKeyIncludingSubkeys(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client');
end;
end;
end;
+104
View File
@@ -0,0 +1,104 @@
!include "MUI2.nsh"
!include "LogicLib.nsh"
Name "LOGAR Client"
OutFile "../dist/LOGAR-Client-Setup.exe"
InstallDir "$PROGRAMFILES64\LOGAR"
RequestExecutionLevel admin
BrandingText "LOGAR Enterprise SIEM & Edge Forwarder"
VIProductVersion "0.3.1.0"
VIAddVersionKey "ProductName" "LOGAR Client"
VIAddVersionKey "CompanyName" "LOGAR Security Systems"
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR Security Systems"
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
VIAddVersionKey "FileVersion" "0.3.1.0"
VIAddVersionKey "ProductVersion" "0.3.1.0"
Var DATADIR
# --- UI Pages ---
!define MUI_ABORTWARNING
# Welcome Page
!define MUI_WELCOMEPAGE_TITLE "Welcome to LOGAR Client Setup"
!define MUI_WELCOMEPAGE_TEXT "LOGAR Client provides secure, encrypted Windows Event Log forwarding with automatic certificate enrollment and state tracking.\r\n\r\nThis wizard will guide you through installing the forwarder service."
!insertmacro MUI_PAGE_WELCOME
# 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_DIRECTORYPAGE_VARIABLE $INSTDIR
!insertmacro MUI_PAGE_DIRECTORY
# Directory Page 2: Data Directory
!define MUI_PAGE_HEADER_TEXT "Choose Data & Storage Location"
!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_DIRECTORYPAGE_VARIABLE $DATADIR
!insertmacro MUI_PAGE_DIRECTORY
# Instfiles Page
!insertmacro MUI_PAGE_INSTFILES
# Finish Page
!define MUI_FINISHPAGE_TITLE "Installation Complete"
!define MUI_FINISHPAGE_TEXT "LOGAR Client has been successfully installed and configured as an automated background service."
!insertmacro MUI_PAGE_FINISH
# Uninstaller Pages
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
Function .onInit
SetShellVarContext all
StrCpy $DATADIR "$APPDATA\LOGAR"
FunctionEnd
Section "Install"
SetShellVarContext all
SetOutPath "$INSTDIR"
File "../dist/Win_Client.exe"
File "../compilation/nssm.exe"
File /r "../dist/runtime"
CreateDirectory "$DATADIR"
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
# Install and configure service via NSSM
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Client "$INSTDIR\Win_Client.exe"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppDirectory "$INSTDIR"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$DATADIR\client.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$DATADIR\client_err.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppParameters "--config \"$DATADIR\client_config.json\""'
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
WriteUninstaller "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "DataDir" "$DATADIR"
SectionEnd
Section "Uninstall"
SetShellVarContext all
ReadRegStr $DATADIR HKLM "Software\LOGAR\LOGAR-Client" "DataDir"
${If} $DATADIR == ""
StrCpy $DATADIR "$APPDATA\LOGAR"
${EndIf}
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm'
RMDir /r "$INSTDIR\runtime"
RMDir /r "$INSTDIR\certs"
Delete "$INSTDIR\client_config.json"
Delete "$INSTDIR\client_state.json"
Delete "$INSTDIR\*.log"
Delete "$INSTDIR\Win_Client.exe"
Delete "$INSTDIR\nssm.exe"
Delete "$INSTDIR\uninstall.exe"
RMDir /r "$INSTDIR"
RMDir /r "$DATADIR"
DeleteRegKey HKLM "Software\LOGAR\LOGAR-Client"
SectionEnd
+75 -4
View File
@@ -1,27 +1,98 @@
[Setup] [Setup]
AppName=LOGAR Server AppName=LOGAR Server
AppVersion=2.0.1 AppVersion=0.3.1
AppPublisher=LOGAR Security Systems
AppPublisherURL=https://logar.eibl.tech
AppSupportURL=https://logar.eibl.tech
AppUpdatesURL=https://logar.eibl.tech
DefaultDirName={autopf}\LOGAR-Server DefaultDirName={autopf}\LOGAR-Server
OutputDir=..\dist OutputDir=..\dist
OutputBaseFilename=LOGAR-Server-Setup OutputBaseFilename=LOGAR-Server-Setup
PrivilegesRequired=admin PrivilegesRequired=admin
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern
VersionInfoCompany=LOGAR Security Systems
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
VersionInfoVersion=0.3.1.0
VersionInfoProductName=LOGAR Server
VersionInfoProductVersion=0.3.1
[Files] [Files]
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs] [Dirs]
Name: "{commonappdata}\LOGAR-Server"; Permissions: users-modify Name: "{code:GetDataDir}"; Permissions: users-modify
[Run] [Run]
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Server ""{app}\Server.exe"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Server ""{app}\Server.exe"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppDirectory ""{app}"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppDirectory ""{app}"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStdout ""{commonappdata}\LOGAR-Server\server.log"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStdout ""{code:GetDataDir}\server.log"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStderr ""{commonappdata}\LOGAR-Server\server_err.log"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStderr ""{code:GetDataDir}\server_err.log"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppParameters ""--config """"{code:GetDataDir}\server_config.json"""""""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Server"; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Server"; Flags: runhidden
[UninstallRun] [UninstallRun]
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Server"; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Server"; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Server confirm"; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Server confirm"; Flags: runhidden
[UninstallDelete]
Type: filesandordirs; Name: "{app}\certs"
Type: files; Name: "{app}\server_config.json"
Type: files; Name: "{app}\logar_state.db"
Type: files; Name: "{app}\logar_state.db-*"
Type: files; Name: "{app}\*.log"
Type: filesandordirs; Name: "{app}\runtime"
Type: filesandordirs; Name: "{app}"
Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server"
[Code]
var
DataDirPage: TInputDirWizardPage;
procedure InitializeWizard;
begin
DataDirPage := CreateInputDirPage(
wpSelectDir,
'Data and Configuration Location',
'Where should LOGAR Server store its database, certificates, and configuration?',
'Select the folder where LOGAR Server should store the database (logar_state.db), configuration (server_config.json), and mTLS certificates.'#13#10#13#10 +
'By default, this is stored in Common AppData (C:\ProgramData\LOGAR-Server), which is accessible to background Windows services.',
False,
''
);
DataDirPage.Add('Data Directory:');
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
end;
function GetDataDir(Param: String): String;
begin
Result := DataDirPage.Values[0];
if Result = '' then
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
CustomDataDir: String;
begin
if CurUninstallStep = usPostUninstall then
begin
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', CustomDataDir) then
begin
if (CustomDataDir <> '') and DirExists(CustomDataDir) then
DelTree(CustomDataDir, True, True, True);
RegDeleteKeyIncludingSubkeys(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server');
end;
end;
end;
+105
View File
@@ -0,0 +1,105 @@
!include "MUI2.nsh"
!include "LogicLib.nsh"
Name "LOGAR Server"
OutFile "../dist/LOGAR-Server-Setup.exe"
InstallDir "$PROGRAMFILES64\LOGAR-Server"
RequestExecutionLevel admin
BrandingText "LOGAR Enterprise SIEM & Log Ingestion"
VIProductVersion "0.3.1.0"
VIAddVersionKey "ProductName" "LOGAR Server"
VIAddVersionKey "CompanyName" "LOGAR Security Systems"
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR Security Systems"
VIAddVersionKey "FileDescription" "LOGAR Cloud Hub & TCP Ingestion Server Setup"
VIAddVersionKey "FileVersion" "0.3.1.0"
VIAddVersionKey "ProductVersion" "0.3.1.0"
Var DATADIR
# --- UI Pages ---
!define MUI_ABORTWARNING
# Welcome Page
!define MUI_WELCOMEPAGE_TITLE "Welcome to LOGAR Server Setup"
!define MUI_WELCOMEPAGE_TEXT "LOGAR provides high-performance encrypted log ingestion, temporal analysis, and mTLS client enrollment.\r\n\r\nThis wizard will guide you through installing LOGAR Server and configuring it as an automated Windows service."
!insertmacro MUI_PAGE_WELCOME
# 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_DIRECTORYPAGE_VARIABLE $INSTDIR
!insertmacro MUI_PAGE_DIRECTORY
# Directory Page 2: Data Directory
!define MUI_PAGE_HEADER_TEXT "Choose Data & Storage Location"
!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_DIRECTORYPAGE_VARIABLE $DATADIR
!insertmacro MUI_PAGE_DIRECTORY
# Instfiles Page
!insertmacro MUI_PAGE_INSTFILES
# Finish Page
!define MUI_FINISHPAGE_TITLE "Installation Complete"
!define MUI_FINISHPAGE_TEXT "LOGAR Server has been successfully installed and started as a background Windows service.\r\n\r\nWeb Hub Dashboard: http://localhost:8443\r\nTCP Ingestion Port: 9443"
!insertmacro MUI_PAGE_FINISH
# Uninstaller Pages
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
Function .onInit
SetShellVarContext all
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
FunctionEnd
Section "Install"
SetShellVarContext all
SetOutPath "$INSTDIR"
File "../dist/Server.exe"
File "../compilation/nssm.exe"
File /r "../dist/runtime"
CreateDirectory "$DATADIR"
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
# Install and configure service via NSSM
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Server "$INSTDIR\Server.exe"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppDirectory "$INSTDIR"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$DATADIR\server.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStderr "$DATADIR\server_err.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppParameters "--config \"$DATADIR\server_config.json\""'
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
WriteUninstaller "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "DataDir" "$DATADIR"
SectionEnd
Section "Uninstall"
SetShellVarContext all
ReadRegStr $DATADIR HKLM "Software\LOGAR\LOGAR-Server" "DataDir"
${If} $DATADIR == ""
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
${EndIf}
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm'
RMDir /r "$INSTDIR\runtime"
RMDir /r "$INSTDIR\certs"
Delete "$INSTDIR\server_config.json"
Delete "$INSTDIR\logar_state.db"
Delete "$INSTDIR\logar_state.db-*"
Delete "$INSTDIR\*.log"
Delete "$INSTDIR\Server.exe"
Delete "$INSTDIR\nssm.exe"
Delete "$INSTDIR\uninstall.exe"
RMDir /r "$INSTDIR"
RMDir /r "$DATADIR"
DeleteRegKey HKLM "Software\LOGAR\LOGAR-Server"
SectionEnd
+386 -29
View File
@@ -12,6 +12,7 @@ SRC_DIR = os.path.join(ROOT_DIR, "src")
DIST_DIR = os.path.join(ROOT_DIR, "dist") DIST_DIR = os.path.join(ROOT_DIR, "dist")
BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp") BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
def clean_and_prep(): def clean_and_prep():
if os.path.exists(DIST_DIR): if os.path.exists(DIST_DIR):
shutil.rmtree(DIST_DIR) shutil.rmtree(DIST_DIR)
@@ -20,6 +21,22 @@ def clean_and_prep():
shutil.rmtree(BUILD_TEMP) shutil.rmtree(BUILD_TEMP)
os.makedirs(BUILD_TEMP, exist_ok=True) os.makedirs(BUILD_TEMP, exist_ok=True)
def calculate_sha256(filepath):
h = hashlib.sha256()
with open(filepath, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
def write_checksum_file(filename, digest, binary_filename):
out_path = os.path.join(DIST_DIR, filename)
with open(out_path, "w", encoding="utf-8") as f:
f.write(f"{digest} {binary_filename}\n")
print(f"[+] Generated checksum file: {filename} ({digest[:16]}...)")
def build_pyinstaller_binary(script_path, binary_name): def build_pyinstaller_binary(script_path, binary_name):
print(f"[*] Compiling {binary_name} with PyInstaller...") print(f"[*] Compiling {binary_name} with PyInstaller...")
cmd = [ cmd = [
@@ -38,25 +55,15 @@ def build_pyinstaller_binary(script_path, binary_name):
raise RuntimeError(f"Failed to build {binary_name}") raise RuntimeError(f"Failed to build {binary_name}")
print(f"[+] Successfully compiled {binary_name}") print(f"[+] Successfully compiled {binary_name}")
def calculate_sha256(filepath):
h = hashlib.sha256()
with open(filepath, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
def write_checksum_file(filename, digest, binary_filename):
out_path = os.path.join(DIST_DIR, filename)
with open(out_path, "w", encoding="utf-8") as f:
f.write(f"{digest} {binary_filename}\n")
print(f"[+] Generated checksum file: {filename} ({digest[:16]}...)")
def build_linux_zipapp_fallback(): def build_linux_zipapp_fallback():
print("[*] Packaging Linux standalone zipapp fallback binaries...") print("[*] Packaging Linux standalone zipapp fallback binaries...")
# Linux Client zipapp # Linux Client zipapp
app_dir = os.path.join(BUILD_TEMP, "linux_app") app_dir = os.path.join(BUILD_TEMP, "linux_app")
os.makedirs(app_dir, exist_ok=True) os.makedirs(app_dir, exist_ok=True)
shutil.copy(os.path.join(SRC_DIR, "Linux_Client.py"), os.path.join(app_dir, "Linux_Client.py")) for f in os.listdir(SRC_DIR):
if f.endswith(".py"):
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_dir, f))
client_out = os.path.join(DIST_DIR, "Linux_Client.bin") client_out = os.path.join(DIST_DIR, "Linux_Client.bin")
zipapp.create_archive( zipapp.create_archive(
source=app_dir, source=app_dir,
@@ -67,7 +74,9 @@ def build_linux_zipapp_fallback():
# Server zipapp # Server zipapp
srv_dir = os.path.join(BUILD_TEMP, "linux_srv") srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
os.makedirs(srv_dir, exist_ok=True) os.makedirs(srv_dir, exist_ok=True)
shutil.copy(os.path.join(SRC_DIR, "Server.py"), os.path.join(srv_dir, "Server.py")) for f in os.listdir(SRC_DIR):
if f.endswith(".py"):
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(srv_dir, f))
server_out = os.path.join(DIST_DIR, "Server.bin") server_out = os.path.join(DIST_DIR, "Server.bin")
zipapp.create_archive( zipapp.create_archive(
source=srv_dir, source=srv_dir,
@@ -76,16 +85,297 @@ def build_linux_zipapp_fallback():
main="Server:main" main="Server:main"
) )
def build_windows():
print("[*] Compiling Windows standalone executables...")
win_client_script = os.path.join(SRC_DIR, "Win_Client.py")
build_pyinstaller_binary(win_client_script, "Win_Client")
server_script = os.path.join(SRC_DIR, "Server.py") def prepare_windows_runtime():
build_pyinstaller_binary(server_script, "Server") """
Downloads the official Python 3.11 embeddable 64-bit runtime, configures ._pth,
and downloads/unpacks all required dependencies (wheels) for standalone execution.
"""
runtime_dir = os.path.join(DIST_DIR, "runtime")
if os.path.exists(runtime_dir):
return runtime_dir
os.makedirs(runtime_dir, exist_ok=True)
site_packages = os.path.join(runtime_dir, "site-packages")
os.makedirs(site_packages, exist_ok=True)
# 1. Download & Extract Python 3.11 embeddable zip
embed_zip_name = "python-3.11.9-embed-amd64.zip"
embed_zip_path = os.path.join(BUILD_TEMP, embed_zip_name)
embed_url = f"https://www.python.org/ftp/python/3.11.9/{embed_zip_name}"
if not os.path.exists(embed_zip_path):
print(f"[*] Downloading Python 3.11 embeddable runtime from {embed_url}...")
import urllib.request
urllib.request.urlretrieve(embed_url, embed_zip_path)
import zipfile
with zipfile.ZipFile(embed_zip_path, "r") as zf:
zf.extractall(runtime_dir)
# 2. Configure python311._pth
pth_file = os.path.join(runtime_dir, "python311._pth")
with open(pth_file, "w", encoding="utf-8") as f:
f.write("python311.zip\n.\nsite-packages\nsite-packages/win32\nsite-packages/win32/lib\nsite-packages/Pythonwin\nimport site\n")
# 3. Download and unpack Windows AMD64 dependencies
wheels_dir = os.path.join(BUILD_TEMP, "win_wheels")
os.makedirs(wheels_dir, exist_ok=True)
print("[*] Fetching required Windows x64 dependency wheels...")
# Build PGPy 0.6.0 wheel from git (supports modern cryptography)
try:
subprocess.run([
sys.executable, "-m", "pip", "wheel", "--no-deps",
"git+https://github.com/SecurityInnovation/PGPy.git",
"--wheel-dir", wheels_dir
], check=False, capture_output=True)
except Exception:
pass
# Download binary wheels for win_amd64 cp311
deps = [
"cryptography", "fastapi", "uvicorn", "pydantic",
"pywin32", "websockets", "pyasn1", "six", "cffi", "pycparser"
]
pip_cmd = [
sys.executable, "-m", "pip", "download",
"--only-binary=:all:",
"--platform", "win_amd64",
"--python-version", "311",
"--implementation", "cp",
"--abi", "cp311",
"--dest", wheels_dir
] + deps
subprocess.run(pip_cmd, check=False, capture_output=True)
for whl in os.listdir(wheels_dir):
if whl.endswith(".whl"):
whl_path = os.path.join(wheels_dir, whl)
with zipfile.ZipFile(whl_path, "r") as zf:
zf.extractall(site_packages)
# 4. Copy pywin32 system DLLs to runtime root for immediate DLL loader resolution
sys32_dir = os.path.join(site_packages, "pywin32_system32")
if os.path.exists(sys32_dir):
for dll in os.listdir(sys32_dir):
if dll.lower().endswith(".dll"):
shutil.copy(os.path.join(sys32_dir, dll), runtime_dir)
print("[+] Successfully assembled standalone Windows Python 3.11 runtime environment.")
return runtime_dir
def create_windows_exe_zipapp(script_name, exe_name):
"""
Cross-compilation helper: Creates a standalone Windows .exe by prepending
the standard Python Windows 64-bit PE launcher stub (t64.exe) to a compiled zipapp archive.
"""
app_temp_dir = os.path.join(BUILD_TEMP, f"win_app_{exe_name}")
os.makedirs(app_temp_dir, exist_ok=True)
for f in os.listdir(SRC_DIR):
if f.endswith(".py"):
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_temp_dir, f))
module_name = os.path.splitext(script_name)[0]
zipapp_temp = os.path.join(BUILD_TEMP, f"{exe_name}.zip")
zipapp.create_archive(
source=app_temp_dir,
target=zipapp_temp,
interpreter=r"runtime\python.exe",
main=f"{module_name}:main"
)
stub_bytes = b""
try:
import pip._vendor.distlib.resources as distlib_res
finder = distlib_res.finder("pip._vendor.distlib")
res = finder.find("t64.exe")
if res:
stub_bytes = res.bytes
except Exception:
pass
if not stub_bytes:
try:
import distlib.resources as distlib_res
finder = distlib_res.finder("distlib")
res = finder.find("t64.exe")
if res:
stub_bytes = res.bytes
except Exception:
pass
out_exe_path = os.path.join(DIST_DIR, f"{exe_name}.exe")
with open(out_exe_path, "wb") as out_f:
if stub_bytes:
out_f.write(stub_bytes)
with open(zipapp_temp, "rb") as in_f:
out_f.write(in_f.read())
print(f"[+] Successfully cross-compiled Windows PE executable: {exe_name}.exe (PE Stub attached: {bool(stub_bytes)})")
def ensure_makensis():
"""Finds or attempts to install makensis on the current system."""
for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis",
r"C:\Program Files (x86)\NSIS\makensis.exe",
r"C:\Program Files\NSIS\makensis.exe"]:
p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd
if p and (os.path.exists(p) or shutil.which(p)):
return p
if platform.system() == "Linux":
print("[*] Attempting to install 'nsis' package dynamically...")
sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else []
if shutil.which("apt-get"):
try:
subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False)
subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False)
subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", "nsis", "nsis-common"], check=False)
except Exception as e:
print(f"[!] Warning: Dynamic apt install failed: {e}")
elif shutil.which("apk"):
try:
subprocess.run(sudo + ["apk", "add", "--no-cache", "nsis"], check=False)
except Exception:
pass
elif shutil.which("dnf"):
try:
subprocess.run(sudo + ["dnf", "install", "-y", "mingw32-nsis", "nsis"], check=False)
except Exception:
pass
for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis"]:
p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd
if p and (os.path.exists(p) or shutil.which(p)):
return p
return None
def ensure_libpython():
"""Attempts to install libpython shared library on Debian/Ubuntu for PyInstaller."""
if platform.system() == "Linux" and shutil.which("apt-get"):
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else []
try:
subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False)
subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False)
subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", f"libpython{py_ver}", "python3-dev", f"python{py_ver}-dev", "libpython3-dev"], check=False)
except Exception:
pass
def compile_nsis_installers():
"""Compiles NSIS installers if makensis is available (native Linux/Windows)."""
makensis_bin = ensure_makensis()
if not makensis_bin:
print("[!] Note: NSIS compiler (makensis) not found in PATH or standard directories.")
return False
makensis_cmd = [makensis_bin]
compilation_dir = os.path.join(ROOT_DIR, "compilation")
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]}...")
try:
subprocess.run(makensis_cmd + [nsi_path], cwd=compilation_dir, check=True)
print(f"[+] Successfully compiled NSIS installer for {nsi}")
except Exception as e:
print(f"[!] Warning: Failed to compile {nsi} with {makensis_cmd}: {e}")
success = False
return success
def compile_inno_setup_installers():
"""Compiles Inno Setup installers if iscc or innosetup wrapper is available."""
iscc_cmd = None
user_iscc = os.path.expanduser(r"~\AppData\Local\Programs\Inno Setup 6\ISCC.exe")
if shutil.which("iscc"):
iscc_cmd = ["iscc"]
elif shutil.which("innosetup"):
iscc_cmd = ["innosetup"]
elif os.path.exists(user_iscc):
iscc_cmd = [user_iscc]
elif os.path.exists("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"):
iscc_cmd = ["C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"]
elif os.path.exists("C:\\Program Files\\Inno Setup 6\\ISCC.exe"):
iscc_cmd = ["C:\\Program Files\\Inno Setup 6\\ISCC.exe"]
elif shutil.which("wine") and os.path.exists("/opt/innosetup/app/ISCC.exe"):
iscc_cmd = ["wine", "/opt/innosetup/app/ISCC.exe"]
elif shutil.which("wine"):
wine_iscc = os.path.expanduser("~/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe")
if os.path.exists(wine_iscc):
iscc_cmd = ["wine", wine_iscc]
if not iscc_cmd:
print("[!] Note: Inno Setup compiler (iscc / innosetup) not available on host. Skipping installer .exe creation.")
return False
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}...")
try:
subprocess.run(iscc_cmd + [iss_path], check=True)
print(f"[+] Successfully compiled {iss}")
except Exception as e:
print(f"[!] Warning: Failed to compile {iss} with {iscc_cmd}: {e}")
return True
def compile_windows_installers():
if not compile_nsis_installers():
compile_inno_setup_installers()
def build_windows():
print("[*] Packaging self-contained Windows standalone executables...")
prepare_windows_runtime()
create_windows_exe_zipapp("Win_Client.py", "Win_Client")
create_windows_exe_zipapp("Server.py", "Server")
compile_windows_installers()
client_bin = os.path.join(DIST_DIR, "Win_Client.exe") client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
server_bin = os.path.join(DIST_DIR, "Server.exe") server_bin = os.path.join(DIST_DIR, "Server.exe")
runtime_dir = os.path.join(DIST_DIR, "runtime")
# Windows Zip Bundles
import zipfile
if os.path.exists(client_bin):
client_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Client.zip")
with zipfile.ZipFile(client_zip, "w", zipfile.ZIP_DEFLATED) as zf:
zf.write(client_bin, arcname="Win_Client.exe")
sample_cfg = os.path.join(ROOT_DIR, "out", "win_client", "client_config.sample.json")
if os.path.exists(sample_cfg):
zf.write(sample_cfg, arcname="client_config.sample.json")
if os.path.exists(runtime_dir):
for root, _, files in os.walk(runtime_dir):
for file in files:
full_p = os.path.join(root, file)
rel_p = os.path.relpath(full_p, DIST_DIR)
zf.write(full_p, arcname=rel_p)
print(f"[+] Packaged Windows Client Zip: {os.path.basename(client_zip)}")
if os.path.exists(server_bin):
server_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Server.zip")
with zipfile.ZipFile(server_zip, "w", zipfile.ZIP_DEFLATED) as zf:
zf.write(server_bin, arcname="Server.exe")
sample_cfg = os.path.join(ROOT_DIR, "server_config.sample.json")
if os.path.exists(sample_cfg):
zf.write(sample_cfg, arcname="server_config.sample.json")
if os.path.exists(runtime_dir):
for root, _, files in os.walk(runtime_dir):
for file in files:
full_p = os.path.join(root, file)
rel_p = os.path.relpath(full_p, DIST_DIR)
zf.write(full_p, arcname=rel_p)
print(f"[+] Packaged Windows Server Zip: {os.path.basename(server_zip)}")
sums = [] sums = []
if os.path.exists(client_bin): if os.path.exists(client_bin):
@@ -103,20 +393,33 @@ def build_windows():
else: else:
print(f"[!] Warning: Expected {server_bin} was not found.") print(f"[!] Warning: Expected {server_bin} was not found.")
# Inno Setup output checksums if present
for inst in ["LOGAR-Client-Setup.exe", "LOGAR-Server-Setup.exe", "LOGAR-Windows-Client.zip", "LOGAR-Windows-Server.zip"]:
inst_path = os.path.join(DIST_DIR, inst)
if os.path.exists(inst_path):
inst_hash = calculate_sha256(inst_path)
sums.append(f"{inst_hash} {inst}")
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_windows.txt") sums_path = os.path.join(DIST_DIR, "SHA256SUMS_windows.txt")
with open(sums_path, "w", encoding="utf-8") as f: with open(sums_path, "w", encoding="utf-8") as f:
f.write("\n".join(sums) + "\n") f.write("\n".join(sums) + "\n")
def build_linux(): def build_linux():
print("[*] Compiling Linux standalone binaries...") print("[*] Compiling Linux standalone binaries and packaging installers...")
is_linux_host = platform.system() == "Linux" is_linux_host = platform.system() == "Linux"
if is_linux_host: if is_linux_host:
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py") ensure_libpython()
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin") try:
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py")
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
server_script = os.path.join(SRC_DIR, "Server.py") server_script = os.path.join(SRC_DIR, "Server.py")
build_pyinstaller_binary(server_script, "Server.bin") build_pyinstaller_binary(server_script, "Server.bin")
except Exception as e:
print(f"[!] PyInstaller compilation encountered an issue ({e}). Falling back to standalone zipapp packaging...")
build_linux_zipapp_fallback()
# Normalize extensions in case PyInstaller dropped .bin # Normalize extensions in case PyInstaller dropped .bin
for name in ["Linux_Client", "Server"]: for name in ["Linux_Client", "Server"]:
@@ -137,9 +440,37 @@ def build_linux():
print("[!] Note: Host platform is not Linux. Generating executable zipapps for Linux target.") print("[!] Note: Host platform is not Linux. Generating executable zipapps for Linux target.")
build_linux_zipapp_fallback() build_linux_zipapp_fallback()
# Copy Linux installation scripts directly to dist
for sh in ["install_linux_client.sh", "install_linux_server.sh"]:
sh_src = os.path.join(ROOT_DIR, "compilation", sh)
if os.path.exists(sh_src):
sh_dst = os.path.join(DIST_DIR, sh)
shutil.copy(sh_src, sh_dst)
print(f"[+] Copied Linux installer script: {sh}")
# Package Linux tarballs
import tarfile
client_bin = os.path.join(DIST_DIR, "Linux_Client.bin") client_bin = os.path.join(DIST_DIR, "Linux_Client.bin")
server_bin = os.path.join(DIST_DIR, "Server.bin") server_bin = os.path.join(DIST_DIR, "Server.bin")
if os.path.exists(client_bin):
client_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Client.tar.gz")
with tarfile.open(client_tar, "w:gz") as tf:
tf.add(client_bin, arcname="Linux_Client.bin")
client_sh = os.path.join(DIST_DIR, "install_linux_client.sh")
if os.path.exists(client_sh):
tf.add(client_sh, arcname="install_linux_client.sh")
print(f"[+] Packaged Linux Client Tarball: {os.path.basename(client_tar)}")
if os.path.exists(server_bin):
server_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Server.tar.gz")
with tarfile.open(server_tar, "w:gz") as tf:
tf.add(server_bin, arcname="Server.bin")
server_sh = os.path.join(DIST_DIR, "install_linux_server.sh")
if os.path.exists(server_sh):
tf.add(server_sh, arcname="install_linux_server.sh")
print(f"[+] Packaged Linux Server Tarball: {os.path.basename(server_tar)}")
sums = [] sums = []
if os.path.exists(client_bin): if os.path.exists(client_bin):
client_hash = calculate_sha256(client_bin) client_hash = calculate_sha256(client_bin)
@@ -156,16 +487,39 @@ def build_linux():
else: else:
print(f"[!] Warning: Expected {server_bin} was not found.") print(f"[!] Warning: Expected {server_bin} was not found.")
for item in ["install_linux_client.sh", "install_linux_server.sh", "LOGAR-Linux-Client.tar.gz", "LOGAR-Linux-Server.tar.gz"]:
item_path = os.path.join(DIST_DIR, item)
if os.path.exists(item_path):
item_hash = calculate_sha256(item_path)
sums.append(f"{item_hash} {item}")
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_linux.txt") sums_path = os.path.join(DIST_DIR, "SHA256SUMS_linux.txt")
with open(sums_path, "w", encoding="utf-8") as f: with open(sums_path, "w", encoding="utf-8") as f:
f.write("\n".join(sums) + "\n") f.write("\n".join(sums) + "\n")
def build_all():
print("[*] Building standalone distribution artifacts for both Linux and Windows...")
build_linux()
build_windows()
# Consolidated master checksums file
all_sums = []
for f in sorted(os.listdir(DIST_DIR)):
fpath = os.path.join(DIST_DIR, f)
if os.path.isfile(fpath) and not f.endswith(".txt") and not "sha256sum" in f:
h = calculate_sha256(fpath)
all_sums.append(f"{h} {f}")
with open(os.path.join(DIST_DIR, "SHA256SUMS.txt"), "w", encoding="utf-8") as f:
f.write("\n".join(all_sums) + "\n")
def main(target=None): def main(target=None):
if target is None: if target is None:
parser = argparse.ArgumentParser(description="LOGAR Standalone Binary Compiler & Packager") parser = argparse.ArgumentParser(description="LOGAR Standalone Binary Compiler & Packager")
parser.add_argument( parser.add_argument(
"--target", "-t", "--target", "-t",
choices=["windows", "win", "linux", "auto"], choices=["all", "windows", "win", "linux", "auto"],
default="auto", default="auto",
help="Target platform to compile binaries for (default: auto-detect)" help="Target platform to compile binaries for (default: auto-detect)"
) )
@@ -173,19 +527,21 @@ def main(target=None):
target = args.target target = args.target
if target == "auto": if target == "auto":
target = "windows" if platform.system() == "Windows" else "linux" target = "all"
elif target == "win": elif target == "win":
target = "windows" target = "windows"
print("=" * 60) print("=" * 60)
print(" LOGAR Binary Packaging") print(" LOGAR Binary Packaging & Cross-Compilation")
print(f" Host Platform: {platform.system()} ({platform.machine()})") print(f" Host Platform: {platform.system()} ({platform.machine()})")
print(f" Target Platform: {target.upper()}") print(f" Target Platform: {target.upper()}")
print("=" * 60) print("=" * 60)
clean_and_prep() clean_and_prep()
if target == "windows": if target == "all":
build_all()
elif target == "windows":
build_windows() build_windows()
elif target == "linux": elif target == "linux":
build_linux() build_linux()
@@ -202,5 +558,6 @@ def main(target=None):
print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)") print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)")
print("=" * 60) print("=" * 60)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+2
View File
@@ -3,5 +3,7 @@ standard-imghdr>=3.13.0; python_version >= "3.13"
cryptography>=42.0.0 cryptography>=42.0.0
fastapi>=0.110.0 fastapi>=0.110.0
uvicorn>=0.28.0 uvicorn>=0.28.0
websockets>=11.0.0
pydantic>=2.6.0 pydantic>=2.6.0
distlib>=0.3.8
pywin32>=306; sys_platform == "win32" pywin32>=306; sys_platform == "win32"
+17 -5
View File
@@ -13,9 +13,21 @@ import time
DEFAULT_GITEA_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.eibl.tech") DEFAULT_GITEA_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.eibl.tech")
DEFAULT_REPO = os.environ.get("GITEA_REPOSITORY", "me0nline/LOGAR") DEFAULT_REPO = os.environ.get("GITEA_REPOSITORY", "me0nline/LOGAR")
import base64
def get_auth_header(token):
if not token:
return ""
if token.startswith("Basic ") or token.startswith("token ") or token.startswith("Bearer "):
return token
if ":" in token:
b64 = base64.b64encode(token.encode("utf-8")).decode("ascii")
return f"Basic {b64}"
return f"token {token}"
def get_existing_assets(base_url, repo, release_id, token): def get_existing_assets(base_url, repo, release_id, token):
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets" url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
req = urllib.request.Request(url, headers={"Authorization": f"token {token}", "Accept": "application/json"}) req = urllib.request.Request(url, headers={"Authorization": get_auth_header(token), "Accept": "application/json"})
try: try:
with urllib.request.urlopen(req) as resp: with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8")) return json.loads(resp.read().decode("utf-8"))
@@ -24,7 +36,7 @@ def get_existing_assets(base_url, repo, release_id, token):
def delete_asset(base_url, repo, release_id, asset_id, token): def delete_asset(base_url, repo, release_id, asset_id, token):
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}" url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}"
req = urllib.request.Request(url, method="DELETE", headers={"Authorization": f"token {token}"}) req = urllib.request.Request(url, method="DELETE", headers={"Authorization": get_auth_header(token)})
try: try:
with urllib.request.urlopen(req) as resp: with urllib.request.urlopen(req) as resp:
pass pass
@@ -47,7 +59,7 @@ def upload_file_to_release(base_url, repo, release_id, token, file_path, max_ret
for attempt in range(1, max_retries + 1): for attempt in range(1, max_retries + 1):
req = urllib.request.Request(url, data=file_bytes, method="POST") req = urllib.request.Request(url, data=file_bytes, method="POST")
req.add_header("Authorization", f"token {token}") req.add_header("Authorization", get_auth_header(token))
req.add_header("Content-Type", "application/octet-stream") req.add_header("Content-Type", "application/octet-stream")
req.add_header("Accept", "application/json") req.add_header("Accept", "application/json")
@@ -73,14 +85,14 @@ def upload_file_to_release(base_url, repo, release_id, token, file_path, max_ret
def create_or_get_release(base_url, repo, tag, token, title=None, notes=None): def create_or_get_release(base_url, repo, tag, token, title=None, notes=None):
url = f"{base_url}/api/v1/repos/{repo}/releases" url = f"{base_url}/api/v1/repos/{repo}/releases"
headers = { headers = {
"Authorization": f"token {token}", "Authorization": get_auth_header(token),
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json" "Accept": "application/json"
} }
# Check if release exists # Check if release exists
check_url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{urllib.parse.quote(tag)}" check_url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{urllib.parse.quote(tag)}"
check_req = urllib.request.Request(check_url, headers={"Authorization": f"token {token}"}) check_req = urllib.request.Request(check_url, headers={"Authorization": get_auth_header(token)})
try: try:
with urllib.request.urlopen(check_req) as resp: with urllib.request.urlopen(check_req) as resp:
existing = json.loads(resp.read().decode("utf-8")) existing = json.loads(resp.read().decode("utf-8"))
+2 -1
View File
@@ -14,8 +14,9 @@ Standalone compiled binary and automated systemd service distribution for Linux
- **Proactive Expiry Check & Reactive Self-Healing**: Before each run, the client evaluates `client.crt` validity. If within 30 days of expiry, it automatically contacts the hub to renew certificates. If the server Root CA rotates or a TLS handshake error occurs, the client catch-heals by re-enrolling immediately and re-establishing connection without human intervention. - **Proactive Expiry Check & Reactive Self-Healing**: Before each run, the client evaluates `client.crt` validity. If within 30 days of expiry, it automatically contacts the hub to renew certificates. If the server Root CA rotates or a TLS handshake error occurs, the client catch-heals by re-enrolling immediately and re-establishing connection without human intervention.
- **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR` (`journalctl -p warning`). Drops debug noise and skips events older than 24 hours. - **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR` (`journalctl -p warning`). Drops debug noise and skips events older than 24 hours.
- **State Tracking & Deduplication**: Maintains persistent client state in `client_state.json` (tracking systemd journalctl cursors and microsecond timestamps) so every log record is forwarded exactly once without duplicates. - **State Tracking & Deduplication**: Maintains persistent client state in `client_state.json` (tracking systemd journalctl cursors and microsecond timestamps) so every log record is forwarded exactly once without duplicates.
- **Fail-Safe State Commit**: State is committed only when the server returns a verified `success` response. In the event of a network outage, state remains unchanged and unsent events are retried automatically on the next run.
- **Fleet Seat Accounting & License Revocation**: Each enrolled client consumes 1 seat from the hub's master license. If revoked by an administrator via the hub WebUI, socket connections are immediately dropped and blocked until reactivated. - **Fleet Seat Accounting & License Revocation**: Each enrolled client consumes 1 seat from the hub's master license. If revoked by an administrator via the hub WebUI, socket connections are immediately dropped and blocked until reactivated.
- **Dynamic Seat Expansion & Automatic Hub Synchronization**: The forwarder benefits from the hub's dynamic seat quota expansion; as the hub's license seats are increased and synced with the master authority (`logar.eibl.tech`), edge capacity expands automatically without requiring forwarder configuration changes.
- **Master Authority High Availability (Portainer GitOps)**: The authoritative license server at `logar.eibl.tech` runs containerized via Portainer GitOps with automated pipeline builds, ensuring high availability for client seat verification and renewals.
--- ---
+15 -6
View File
@@ -14,7 +14,7 @@ Standalone compiled binary and automated systemd service distribution for Linux
- **Master License Authority (`logar.eibl.tech`) & Seat-Bearing CA**: Integrates with the upstream authority. Subordinate CA certificates embed exact seat quotas via custom enterprise X.509 OID `1.3.6.1.4.1.59999.1.1`. - **Master License Authority (`logar.eibl.tech`) & Seat-Bearing CA**: Integrates with the upstream authority. Subordinate CA certificates embed exact seat quotas via custom enterprise X.509 OID `1.3.6.1.4.1.59999.1.1`.
- **Zero-Touch mTLS Renewals**: Once initial signed CA cert is imported, the server hub automatically executes in-flight mTLS renewals with `https://logar.eibl.tech/api/license/renew` when nearing expiration (< 30 days). - **Zero-Touch mTLS Renewals**: Once initial signed CA cert is imported, the server hub automatically executes in-flight mTLS renewals with `https://logar.eibl.tech/api/license/renew` when nearing expiration (< 30 days).
- **Fleet Management & WebUI Dashboard (Port 8443)**: Modern browser WebUI accessible at `http://localhost:8443/webui` to monitor seat quotas, export `.lic` request files, copy CA CSRs, activate signed CA certs, and revoke/reactivate forwarder seats with immediate socket enforcement. - **Fleet Management & WebUI Dashboard (Port 8443)**: Modern browser WebUI accessible at `http://localhost:8443/webui` to monitor seat quotas, export `.lic` request files, copy CA CSRs, activate signed CA certs, and revoke/reactivate forwarder seats with immediate socket enforcement.
- **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events require persistence across at least 4 distinct client transmission cycles within a rolling 12-hour evaluation window. - **Direct Hermes Intelligence & Temporal Telemetry**: All ingested actionable anomalies (`WARNING`, `ERROR`, `INFO`) are immediately promoted to `VERIFIED` and surfaced directly to Hermes with rich temporal metadata (run counts, timestamps) across a rolling 12-hour evaluation window, allowing Hermes AI to perform all contextual reasoning and decisions.
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`. - **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`.
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`). - **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
@@ -58,7 +58,7 @@ Output:
Transport Security: mTLS (TLS 1.3) Transport Security: mTLS (TLS 1.3)
License Quota: 10 Active Seats License Quota: 10 Active Seats
Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2 Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
Evaluation Window: 12 hours | 4-Run Rule: Warnings | Immediate Pass: Errors Evaluation Window: 12 hours | Direct Hermes Reasoning Enabled
============================================================ ============================================================
[*] LOGAR mTLS TLSv1.3 Socket Server listening on 0.0.0.0:9443 [*] LOGAR mTLS TLSv1.3 Socket Server listening on 0.0.0.0:9443
[*] Hermes Reporting API available at http://0.0.0.0:8443/api/hermes/report [*] Hermes Reporting API available at http://0.0.0.0:8443/api/hermes/report
@@ -81,7 +81,7 @@ The generated `server_config.json` contains:
"tls_enabled": true, "tls_enabled": true,
"db_path": "logar_state.db", "db_path": "logar_state.db",
"evaluation_window_hours": 12, "evaluation_window_hours": 12,
"min_persistence_runs": 4, "min_persistence_runs": 1,
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2", "server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...", "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..." "private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
@@ -110,8 +110,8 @@ http://<SERVER_IP>:8443/webui
The WebUI provides: The WebUI provides:
1. **Real-time Seat Quota Meter**: Visual meter displaying active clients against approved seat quota. 1. **Real-time Seat Quota Meter**: Visual meter displaying active clients against approved seat quota.
2. **Export .lic Request**: 1-click download of `logar_server.lic` containing your Subordinate CA CSR and machine fingerprint for `logar.eibl.tech`. 2. **Export .lic Request**: 1-click download of `logar_server.lic` containing your Subordinate CA CSR and machine fingerprint for `logar.eibl.tech`.
3. **Import Signed CA Certificate**: Modal to paste or upload the signed `ca.crt` issued by `logar.eibl.tech`. Activating updates your seat limit immediately and reloads mTLS context in-flight. 3. **Import License (.lic / ca.crt)**: Modal to paste or upload the activated `.lic` package or `ca.crt` issued by `logar.eibl.tech`. Activating updates your seat limit immediately and reloads mTLS context in-flight.
4. **Trigger mTLS Renewal Now**: Button to test or trigger immediate mTLS renewal against `logar.eibl.tech`. 4. **Sync & Renew via mTLS**: Button to test or trigger immediate mTLS renewal against `logar.eibl.tech` and pull seat quota increases without restart.
5. **Client Seat Management**: View all enrolled nodes; click **Revoke Seat** to immediately drop and block an edge node at the socket layer, or **Reactivate** to restore access. 5. **Client Seat Management**: View all enrolled nodes; click **Revoke Seat** to immediately drop and block an edge node at the socket layer, or **Reactivate** to restore access.
### CLI Licensing Utilities ### CLI Licensing Utilities
@@ -119,10 +119,19 @@ Export a `.lic` request file via CLI:
```bash ```bash
./Server.bin --export-lic server.lic ./Server.bin --export-lic server.lic
``` ```
Import a signed CA certificate via CLI: Import an activated `.lic` license package or signed CA certificate via CLI:
```bash ```bash
./Server.bin --import-lic activated.lic
# Or alternatively with --import-ca:
./Server.bin --import-ca ca.crt ./Server.bin --import-ca ca.crt
``` ```
Synchronize license and check for seat quota increases against the master authority:
```bash
./Server.bin --sync-license
```
### Upstream Authority Deployment (Portainer GitOps & Reverse Proxy)
The upstream Master License Authority service (`logar.eibl.tech`) is compiled and deployed via Portainer GitOps from the [`LOGAR-LICENSING`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git) repository, running from `gitea.eibl.tech/me0nline/logar-licensing:latest` with automated pipeline compilation and persistent volume storage. Public traffic passes through a reverse proxy (e.g., Nginx Proxy Manager with **Websockets Support** enabled), handling WebSocket licensing handshakes (`wss://logar.eibl.tech/ws/license`), multi-tenant fleet isolation, user management, and authenticated WebUI dashboard access.
--- ---
+2 -1
View File
@@ -14,8 +14,9 @@ Standalone compiled executable and installer distribution for Windows Server and
- **Proactive Expiry Check & Reactive Self-Healing**: Before each run, the client evaluates `client.crt` validity. If within 30 days of expiry, it automatically contacts the hub to renew certificates. If the server Root CA rotates or a TLS handshake error occurs, the client catch-heals by re-enrolling immediately and re-establishing connection without human intervention. - **Proactive Expiry Check & Reactive Self-Healing**: Before each run, the client evaluates `client.crt` validity. If within 30 days of expiry, it automatically contacts the hub to renew certificates. If the server Root CA rotates or a TLS handshake error occurs, the client catch-heals by re-enrolling immediately and re-establishing connection without human intervention.
- **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR`. Strips audit events and debug noise, skipping events older than 24 hours. - **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR`. Strips audit events and debug noise, skipping events older than 24 hours.
- **State Tracking & Deduplication**: Maintains persistent client state in `client_state.json` (tracking event record numbers and timestamp signatures) so every log record is forwarded exactly once without duplicates. - **State Tracking & Deduplication**: Maintains persistent client state in `client_state.json` (tracking event record numbers and timestamp signatures) so every log record is forwarded exactly once without duplicates.
- **Fail-Safe State Commit**: State is committed only when the server returns a verified `success` response. In the event of a network outage, state remains unchanged and unsent events are retried automatically on the next run.
- **Fleet Seat Accounting & License Revocation**: Each enrolled client consumes 1 seat from the hub's master license. If revoked by an administrator via the hub WebUI, socket connections are immediately dropped and blocked until reactivated. - **Fleet Seat Accounting & License Revocation**: Each enrolled client consumes 1 seat from the hub's master license. If revoked by an administrator via the hub WebUI, socket connections are immediately dropped and blocked until reactivated.
- **Dynamic Seat Expansion & Automatic Hub Synchronization**: The forwarder benefits from the hub's dynamic seat quota expansion; as the hub's license seats are increased and synced with the master authority (`logar.eibl.tech`), edge capacity expands automatically without requiring forwarder configuration changes.
- **Master Authority High Availability (Portainer GitOps)**: The authoritative license server at `logar.eibl.tech` runs containerized via Portainer GitOps with automated pipeline builds, ensuring high availability for client seat verification and renewals.
--- ---
+15 -6
View File
@@ -14,7 +14,7 @@ Standalone compiled executable and installer distribution for Windows Server env
- **Master License Authority (`logar.eibl.tech`) & Seat-Bearing CA**: Integrates with the upstream authority. Subordinate CA certificates embed exact seat quotas via custom enterprise X.509 OID `1.3.6.1.4.1.59999.1.1`. - **Master License Authority (`logar.eibl.tech`) & Seat-Bearing CA**: Integrates with the upstream authority. Subordinate CA certificates embed exact seat quotas via custom enterprise X.509 OID `1.3.6.1.4.1.59999.1.1`.
- **Zero-Touch mTLS Renewals**: Once initial signed CA cert is imported, the server hub automatically executes in-flight mTLS renewals with `https://logar.eibl.tech/api/license/renew` when nearing expiration (< 30 days). - **Zero-Touch mTLS Renewals**: Once initial signed CA cert is imported, the server hub automatically executes in-flight mTLS renewals with `https://logar.eibl.tech/api/license/renew` when nearing expiration (< 30 days).
- **Fleet Management & WebUI Dashboard (Port 8443)**: Modern browser WebUI accessible at `http://localhost:8443/webui` to monitor seat quotas, export `.lic` request files, copy CA CSRs, activate signed CA certs, and revoke/reactivate forwarder seats with immediate socket enforcement. - **Fleet Management & WebUI Dashboard (Port 8443)**: Modern browser WebUI accessible at `http://localhost:8443/webui` to monitor seat quotas, export `.lic` request files, copy CA CSRs, activate signed CA certs, and revoke/reactivate forwarder seats with immediate socket enforcement.
- **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events require persistence across at least 4 distinct transmission cycles within a rolling 12-hour evaluation window. - **Direct Hermes Intelligence & Temporal Telemetry**: All ingested actionable anomalies (`WARNING`, `ERROR`, `INFO`) are immediately promoted to `VERIFIED` and surfaced directly to Hermes with rich temporal metadata (run counts, timestamps) across a rolling 12-hour evaluation window, allowing Hermes AI to perform all contextual reasoning and decisions.
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`. - **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`.
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`). - **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
@@ -58,7 +58,7 @@ Output:
Transport Security: mTLS (TLS 1.3) Transport Security: mTLS (TLS 1.3)
License Quota: 10 Active Seats License Quota: 10 Active Seats
Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2 Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
Evaluation Window: 12 hours | 4-Run Rule: Warnings | Immediate Pass: Errors Evaluation Window: 12 hours | Direct Hermes Reasoning Enabled
============================================================ ============================================================
[*] LOGAR mTLS TLSv1.3 Socket Server listening on 0.0.0.0:9443 [*] LOGAR mTLS TLSv1.3 Socket Server listening on 0.0.0.0:9443
[*] Hermes Reporting API available at http://0.0.0.0:8443/api/hermes/report [*] Hermes Reporting API available at http://0.0.0.0:8443/api/hermes/report
@@ -81,7 +81,7 @@ The generated `server_config.json` contains:
"tls_enabled": true, "tls_enabled": true,
"db_path": "logar_state.db", "db_path": "logar_state.db",
"evaluation_window_hours": 12, "evaluation_window_hours": 12,
"min_persistence_runs": 4, "min_persistence_runs": 1,
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2", "server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...", "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..." "private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
@@ -110,8 +110,8 @@ http://localhost:8443/webui
The WebUI provides: The WebUI provides:
1. **Real-time Seat Quota Meter**: Visual meter displaying active clients against approved seat quota. 1. **Real-time Seat Quota Meter**: Visual meter displaying active clients against approved seat quota.
2. **Export .lic Request**: 1-click download of `logar_server.lic` containing your Subordinate CA CSR and machine fingerprint for `logar.eibl.tech`. 2. **Export .lic Request**: 1-click download of `logar_server.lic` containing your Subordinate CA CSR and machine fingerprint for `logar.eibl.tech`.
3. **Import Signed CA Certificate**: Modal to paste or upload the signed `ca.crt` issued by `logar.eibl.tech`. Activating updates your seat limit immediately and reloads mTLS context in-flight. 3. **Import License (.lic / ca.crt)**: Modal to paste or upload the activated `.lic` package or `ca.crt` issued by `logar.eibl.tech`. Activating updates your seat limit immediately and reloads mTLS context in-flight.
4. **Trigger mTLS Renewal Now**: Button to test or trigger immediate mTLS renewal against `logar.eibl.tech`. 4. **Sync & Renew via mTLS**: Button to test or trigger immediate mTLS renewal against `logar.eibl.tech` and pull seat quota increases without restart.
5. **Client Seat Management**: View all enrolled nodes; click **Revoke Seat** to immediately drop and block an edge node at the socket layer, or **Reactivate** to restore access. 5. **Client Seat Management**: View all enrolled nodes; click **Revoke Seat** to immediately drop and block an edge node at the socket layer, or **Reactivate** to restore access.
### CLI Licensing Utilities ### CLI Licensing Utilities
@@ -119,10 +119,19 @@ Export a `.lic` request file via CLI:
```powershell ```powershell
.\Server.exe --export-lic C:\LOGAR-Server\server.lic .\Server.exe --export-lic C:\LOGAR-Server\server.lic
``` ```
Import a signed CA certificate via CLI: Import an activated `.lic` license package or signed CA certificate via CLI:
```powershell ```powershell
.\Server.exe --import-lic C:\LOGAR-Server\activated.lic
# Or alternatively with --import-ca:
.\Server.exe --import-ca C:\LOGAR-Server\ca.crt .\Server.exe --import-ca C:\LOGAR-Server\ca.crt
``` ```
Synchronize license and check for seat quota increases against the master authority:
```powershell
.\Server.exe --sync-license
```
### Upstream Authority Deployment (Portainer GitOps & Reverse Proxy)
The upstream Master License Authority service (`logar.eibl.tech`) is compiled and deployed via Portainer GitOps from the [`LOGAR-LICENSING`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git) repository, running from `gitea.eibl.tech/me0nline/logar-licensing:latest` with automated pipeline compilation and persistent volume storage. Public traffic passes through a reverse proxy (e.g., Nginx Proxy Manager with **Websockets Support** enabled), handling WebSocket licensing handshakes (`wss://logar.eibl.tech/ws/license`), multi-tenant fleet isolation, user management, and authenticated WebUI dashboard access.
--- ---
+8
View File
@@ -0,0 +1,8 @@
# Gitea Instance URL
GITEA_INSTANCE_URL=https://gitea.eibl.tech
# Gitea Runner Registration Token (From Gitea Settings -> Actions -> Runners -> Create Runner)
GITEA_RUNNER_REGISTRATION_TOKEN=your_runner_registration_token_here
# Runner Configuration
GITEA_RUNNER_NAME=logar-build-runner
+64
View File
@@ -0,0 +1,64 @@
FROM ubuntu:22.04
LABEL maintainer="me0nline <me0nline@eibl.tech>"
LABEL description="LOGAR CI/CD, Binary Packaging & Release Runner"
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# ── 1. Core System Tools, Python Dev Headers & NSIS Compiler ───────────────────
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
# Basic utilities
ca-certificates \
curl \
wget \
git \
tar \
gzip \
zip \
unzip \
# Native compilation toolchains
build-essential \
gcc \
make \
# Python 3 & dynamic shared libraries (required for PyInstaller standalone binaries)
python3 \
python3-pip \
python3-venv \
python3-dev \
libpython3-dev \
libffi-dev \
libssl-dev \
# NSIS (for compiling Windows Setup executables on Linux)
nsis \
nsis-common \
nsis-pluginapi \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# ── 2. Node.js 20 LTS (required for Act / Gitea Actions runners) ───────────────
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y -qq nodejs \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# ── 3. Pre-installed Python Build & Application Dependencies ───────────────────
RUN pip3 install --no-cache-dir \
# Binary compiler & PE packaging tools
pyinstaller \
distlib \
cryptography \
pefile \
cffi \
# LOGAR core dependencies
pgpy \
fastapi \
uvicorn \
pydantic \
requests \
# Code quality / linter
flake8
WORKDIR /workspace
CMD ["/bin/bash"]
+54
View File
@@ -0,0 +1,54 @@
# LOGAR Custom CI/CD Runner
This directory contains the Docker configuration for the custom **LOGAR Build & Release Runner** (`gitea.eibl.tech/me0nline/logar-runner:latest`).
---
## Included Toolchains & Dependencies
- **Operating System**: Ubuntu 22.04 LTS
- **Python Development**:
- `python3`, `python3-dev`, `libpython3-dev` (shared libraries for native ELF compilation)
- `python3-pip`, `python3-venv`, `python3-setuptools`, `python3-wheel`
- **Native NSIS Cross-Compiler**:
- `nsis`, `nsis-common`, `nsis-pluginapi` (`makensis` CLI for compiling Windows `.exe` installers on Linux)
- **Build Essentials**:
- `gcc`, `make`, `binutils`, `build-essential`, `libffi-dev`, `libssl-dev`
- **Pre-installed Python Packages**:
- `pyinstaller`, `distlib`, `cryptography`, `pefile`, `pgpy`, `fastapi`, `uvicorn`, `pydantic`, `requests`
---
## Workflow Integration
You can run CI/CD jobs directly inside this container image in `.gitea/workflows/`:
```yaml
jobs:
release:
name: Build & Release Standalone Binaries
runs-on: logar
steps:
- uses: actions/checkout@v4
- run: python3 compilation/package_dist.py --target all
```
---
## Building & Pushing the Runner
Trigger the automated workflow [`.gitea/workflows/build-runner.yml`](../.gitea/workflows/build-runner.yml) via the Gitea Actions UI (Actions &rarr; Build and Push LOGAR Runner &rarr; Run Workflow) or push changes to the `runner/` folder.
---
## Deploying the Runner Stack via Portainer
1. In Gitea, navigate to **Site Administration** (or Repo **Settings**) &rarr; **Actions** &rarr; **Runners** &rarr; **Create Runner**. Copy the registration token.
2. In Portainer, navigate to **Stacks** &rarr; **Add stack**.
3. Set the name to `logar-act-runner`.
4. Paste the contents of [`runner/docker-compose.yml`](docker-compose.yml) into the Web editor (or link to this Git repository).
5. In Environment variables, set:
- `GITEA_RUNNER_REGISTRATION_TOKEN`: `<your-gitea-runner-token>`
6. Click **Deploy the stack**.
The runner will automatically register with Gitea under the dedicated label `logar`, executing jobs inside the pre-configured `gitea.eibl.tech/me0nline/logar-runner:latest` container.
+27
View File
@@ -0,0 +1,27 @@
version: '3.8'
services:
logar-runner:
image: gitea/act_runner:latest
container_name: logar-act-runner
restart: unless-stopped
env_file:
- .env
environment:
- GITEA_INSTANCE_URL=${GITEA_INSTANCE_URL:-https://gitea.eibl.tech}
- GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN}
- GITEA_RUNNER_NAME=${GITEA_RUNNER_NAME:-logar-build-runner}
- GITEA_RUNNER_LABELS=logar:docker://gitea.eibl.tech/me0nline/logar-runner:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- logar_runner_data:/data
networks:
- runner-net
volumes:
logar_runner_data:
name: logar_runner_data
networks:
runner-net:
name: logar_runner_net
+16 -624
View File
@@ -1,635 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
LOGAR Master License Authority (logar.eibl.tech) LOGAR Master License Authority Proxy
Delegates directly to the standalone submodule: LOGAR-LICENSING/License_Authority.py
Authoritative PKI service and CLI utility responsible for: Repo: https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git
1. Maintaining the Master Root CA.
2. Processing intermediate CA CSRs (or .lic request files) and issuing signed
subordinate CA certificates embedding exact seat quotas via custom enterprise X.509 OIDs.
3. Providing an automated mTLS renewal endpoint (/api/license/renew) for active servers.
4. Serving an administrative WebUI and REST API to manage and revoke server licenses.
""" """
import os import os
import sys import importlib.util
import json
import sqlite3
import argparse
import datetime
from typing import Tuple, Optional, Dict, Any
from cryptography import x509 ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
from cryptography.x509.oid import NameOID SUBMODULE_FILE = os.path.join(ROOT_DIR, "LOGAR-LICENSING", "License_Authority.py")
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Ensure src/ is importable if not os.path.exists(SUBMODULE_FILE):
SRC_DIR = os.path.dirname(os.path.abspath(__file__)) raise ImportError(f"LOGAR-LICENSING submodule not found at {SUBMODULE_FILE}. Run 'git submodule update --init --recursive'.")
if SRC_DIR not in sys.path:
sys.path.insert(0, SRC_DIR)
import server_enrollment as enrollment spec = importlib.util.spec_from_file_location("_submodule_authority", SUBMODULE_FILE)
_sub = importlib.util.module_from_spec(spec)
try: spec.loader.exec_module(_sub)
from fastapi import FastAPI, HTTPException, Request, Header
from fastapi.responses import HTMLResponse, JSONResponse
import uvicorn
FASTAPI_AVAILABLE = True
except ImportError:
FASTAPI_AVAILABLE = False
DEFAULT_DB = "license_authority.db"
DEFAULT_MASTER_CERT_DIR = "master_ca_certs"
def init_authority_db(db_path: str = DEFAULT_DB):
"""Initializes the SQLite schema for tracking issued server licenses and revocations."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS licenses (
license_id TEXT PRIMARY KEY,
server_fingerprint TEXT,
hostname TEXT,
seats INTEGER NOT NULL,
issued_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
renewal_count INTEGER NOT NULL DEFAULT 0,
cert_pem TEXT NOT NULL,
csr_pem TEXT,
client_cert_fingerprint TEXT
)
""")
conn.commit()
conn.close()
def generate_master_root_ca(
cert_dir: str = DEFAULT_MASTER_CERT_DIR,
common_name: str = "LOGAR-Master-Root-CA",
organization: str = "LOGAR Global Licensing",
country: str = "AT"
) -> Tuple[str, str]:
"""Generates the Master Root CA certificate and private key if not already existing."""
os.makedirs(cert_dir, exist_ok=True)
cert_path = os.path.join(cert_dir, "master_root_ca.crt")
key_path = os.path.join(cert_dir, "master_root_ca.key")
if os.path.exists(cert_path) and os.path.exists(key_path):
with open(cert_path, "r", encoding="utf-8") as f:
cert_pem = f.read()
with open(key_path, "r", encoding="utf-8") as f:
key_pem = f.read()
return cert_pem, key_pem
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, country),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
])
now = datetime.datetime.now(datetime.timezone.utc)
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - datetime.timedelta(minutes=5))
.not_valid_after(now + datetime.timedelta(days=3650))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=False,
key_cert_sign=True,
crl_sign=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()),
critical=False
)
.sign(ca_key, hashes.SHA256())
)
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
key_pem = ca_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
).decode("utf-8")
with open(cert_path, "w", encoding="utf-8") as f:
f.write(cert_pem)
with open(key_path, "w", encoding="utf-8") as f:
f.write(key_pem)
return cert_pem, key_pem
def issue_license_from_csr(
csr_pem: str,
seats: int,
days_valid: int = 365,
license_id: Optional[str] = None,
server_fingerprint: str = "",
hostname: str = "",
db_path: str = DEFAULT_DB,
cert_dir: str = DEFAULT_MASTER_CERT_DIR,
license_server_url: str = "https://logar.eibl.tech"
) -> Dict[str, Any]:
"""Issues a subordinate CA certificate with the requested number of seats and registers it in the DB."""
init_authority_db(db_path)
master_cert_pem, master_key_pem = generate_master_root_ca(cert_dir)
if not license_id:
csr_obj = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
der_pub = csr_obj.public_key().public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo
)
license_id = f"LIC-{der_pub.hex()[:12].upper()}"
ca_cert_pem = enrollment.sign_ca_csr_with_seats(
csr_pem=csr_pem,
master_ca_cert_pem=master_cert_pem,
master_ca_key_pem=master_key_pem,
seats=seats,
days_valid=days_valid,
license_id=license_id,
license_server_url=license_server_url
)
info = enrollment.extract_license_info_from_cert(ca_cert_pem)
issued_at = info["not_valid_before"]
expires_at = info["not_valid_after"]
fp = info["fingerprint"]
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("""
INSERT INTO licenses (license_id, server_fingerprint, hostname, seats, issued_at, expires_at, status, renewal_count, cert_pem, csr_pem, client_cert_fingerprint)
VALUES (?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?)
ON CONFLICT(license_id) DO UPDATE SET
seats = excluded.seats,
expires_at = excluded.expires_at,
status = 'active',
cert_pem = excluded.cert_pem,
client_cert_fingerprint = excluded.client_cert_fingerprint
""", (license_id, server_fingerprint, hostname, seats, issued_at, expires_at, ca_cert_pem, csr_pem, fp))
conn.commit()
conn.close()
return {
"status": "success",
"license_id": license_id,
"seats": seats,
"issued_at": issued_at,
"expires_at": expires_at,
"fingerprint": fp,
"ca_cert": ca_cert_pem,
"master_root_ca": master_cert_pem,
"license_server": license_server_url
}
def renew_license_cert(
current_cert_pem: str,
days_valid: int = 365,
db_path: str = DEFAULT_DB,
cert_dir: str = DEFAULT_MASTER_CERT_DIR,
license_server_url: str = "https://logar.eibl.tech"
) -> Dict[str, Any]:
"""
Validates an existing intermediate CA cert presenting for mTLS renewal,
verifies that the license is active, and issues a renewed certificate preserving seat quota.
"""
init_authority_db(db_path)
master_cert_pem, master_key_pem = generate_master_root_ca(cert_dir)
master_cert = x509.load_pem_x509_certificate(master_cert_pem.encode("utf-8"))
current_cert = x509.load_pem_x509_certificate(current_cert_pem.encode("utf-8"))
# Extract license info
info = enrollment.extract_license_info_from_cert(current_cert_pem)
license_id = info.get("license_id")
seats = info.get("seats") or 10
# Verify license record in database
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("SELECT status, renewal_count, csr_pem, seats FROM licenses WHERE license_id = ?", (license_id,))
row = c.fetchone()
if row:
status, renewals, csr_pem, db_seats = row
if status == "revoked":
conn.close()
raise ValueError(f"License {license_id} is REVOKED and cannot be renewed.")
seats = db_seats or seats
else:
# Auto-enroll license if missing from DB (signed by our master CA)
renewals = 0
csr_pem = None
# Build renewed certificate using the same public key and subject
master_key = serialization.load_pem_private_key(master_key_pem.encode("utf-8"), password=None)
now = datetime.datetime.now(datetime.timezone.utc)
builder = (
x509.CertificateBuilder()
.subject_name(current_cert.subject)
.issuer_name(master_cert.subject)
.public_key(current_cert.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - datetime.timedelta(minutes=5))
.not_valid_after(now + datetime.timedelta(days=days_valid))
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=False,
key_cert_sign=True,
crl_sign=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(current_cert.public_key()),
critical=False
)
.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(master_key.public_key()),
critical=False
)
.add_extension(
x509.UnrecognizedExtension(enrollment.OID_LOGAR_SEATS, str(seats).encode("utf-8")),
critical=False
)
.add_extension(
x509.UnrecognizedExtension(enrollment.OID_LOGAR_LICENSE_ID, license_id.encode("utf-8")),
critical=False
)
.add_extension(
x509.UnrecognizedExtension(enrollment.OID_LOGAR_ISSUER_URL, license_server_url.encode("utf-8")),
critical=False
)
)
new_cert = builder.sign(master_key, hashes.SHA256())
new_pem = new_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
new_info = enrollment.extract_license_info_from_cert(new_pem)
new_fp = new_info["fingerprint"]
c.execute("""
UPDATE licenses
SET renewal_count = renewal_count + 1,
expires_at = ?,
cert_pem = ?,
client_cert_fingerprint = ?
WHERE license_id = ?
""", (new_info["not_valid_after"], new_pem, new_fp, license_id))
conn.commit()
conn.close()
return {
"status": "renewed",
"license_id": license_id,
"seats": seats,
"ca_cert": new_pem,
"expires_at": new_info["not_valid_after"],
"fingerprint": new_fp
}
def revoke_license(license_id: str, db_path: str = DEFAULT_DB) -> bool:
"""Marks a license as revoked in the database."""
init_authority_db(db_path)
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("UPDATE licenses SET status = 'revoked' WHERE license_id = ?", (license_id,))
changed = c.rowcount > 0
conn.commit()
conn.close()
return changed
def list_licenses(db_path: str = DEFAULT_DB) -> list:
"""Lists all registered licenses."""
init_authority_db(db_path)
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT license_id, server_fingerprint, hostname, seats, issued_at, expires_at, status, renewal_count, client_cert_fingerprint FROM licenses ORDER BY issued_at DESC")
rows = [dict(r) for r in c.fetchall()]
conn.close()
return rows
# --- FastAPI Web Service (for deployment at logar.eibl.tech) ---
if FASTAPI_AVAILABLE:
app = FastAPI(title="LOGAR License Authority", version="2.0.1")
@app.post("/api/license/issue")
async def api_issue(request: Request):
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body")
csr_pem = body.get("csr")
seats = int(body.get("seats", 10))
server_fp = body.get("server_fingerprint", "")
hostname = body.get("hostname", "")
lic_id = body.get("license_id")
if not csr_pem:
raise HTTPException(status_code=400, detail="Missing 'csr' in request")
try:
result = issue_license_from_csr(
csr_pem=csr_pem,
seats=seats,
license_id=lic_id,
server_fingerprint=server_fp,
hostname=hostname
)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/license/renew")
async def api_renew(request: Request):
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body")
current_cert = body.get("current_cert")
if not current_cert:
raise HTTPException(status_code=400, detail="Missing 'current_cert' in request")
try:
result = renew_license_cert(current_cert)
return result
except ValueError as ve:
raise HTTPException(status_code=403, detail=str(ve))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/licenses")
def api_list():
return {"licenses": list_licenses()}
@app.post("/api/license/revoke")
async def api_revoke(request: Request):
body = await request.json()
lic_id = body.get("license_id")
if not lic_id:
raise HTTPException(status_code=400, detail="Missing 'license_id'")
ok = revoke_license(lic_id)
if not ok:
raise HTTPException(status_code=404, detail="License not found")
return {"status": "revoked", "license_id": lic_id}
@app.get("/", response_class=HTMLResponse)
def web_dashboard():
licenses = list_licenses()
rows = []
for l in licenses:
badge_cls = "badge-active" if l["status"] == "active" else "badge-revoked"
rows.append(f"""
<tr>
<td><code>{l['license_id']}</code></td>
<td><strong>{l['seats']}</strong></td>
<td><span class="badge {badge_cls}">{l['status'].upper()}</span></td>
<td>{l['hostname'] or 'N/A'}</td>
<td><code>{l['client_cert_fingerprint'][:16]}...</code></td>
<td>{l['expires_at'][:10]}</td>
<td>{l['renewal_count']}</td>
<td>
<button class="btn btn-danger" onclick="revokeLic('{l['license_id']}')">Revoke</button>
</td>
</tr>
""")
rows_html = "".join(rows) if rows else "<tr><td colspan='8' style='text-align: center; color: #64748b;'>No licenses issued yet.</td></tr>"
html_tpl = """<!DOCTYPE html>
<html>
<head>
<title>LOGAR Master License Authority (logar.eibl.tech)</title>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; margin: 0; padding: 2rem; }
.container { max-width: 1100px; margin: 0 auto; }
h1 { color: #38bdf8; display: flex; align-items: center; gap: 10px; }
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; margin-bottom: 2rem; border: 1px solid #334155; }
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid #334155; font-size: 0.9rem; }
th { color: #94a3b8; font-weight: 600; text-transform: uppercase; font-size: 0.75rem; }
.badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-weight: bold; font-size: 0.75rem; }
.badge-active { background: #065f46; color: #34d399; }
.badge-revoked { background: #7f1d1d; color: #f87171; }
code { background: #0f172a; padding: 2px 6px; border-radius: 4px; font-family: monospace; color: #a5b4fc; }
.btn { padding: 0.4rem 0.8rem; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; }
.btn-primary { background: #0284c7; color: white; }
.btn-danger { background: #dc2626; color: white; }
textarea, input { background: #0f172a; border: 1px solid #475569; color: white; padding: 0.5rem; border-radius: 4px; width: 100%; box-sizing: border-box; }
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.5rem; color: #cbd5e1; font-size: 0.85rem; }
</style>
</head>
<body>
<div class="container">
<h1><span>🛡️</span> LOGAR Master License Authority</h1>
<p style="color: #94a3b8;">Authoritative PKI for issuing seat-bearing Intermediate CA certificates and automatic mTLS renewals.</p>
<div class="card">
<h3>Issue Subordinate CA Certificate from CSR / .lic</h3>
<div class="form-group">
<label>Seats Quota (Exact Seat Limit)</label>
<input type="number" id="seats" value="25" min="1" max="10000" style="max-width: 200px;">
</div>
<div class="form-group">
<label>CSR (PEM) or .lic JSON content</label>
<textarea id="csrText" rows="6" placeholder="-----BEGIN CERTIFICATE REQUEST----- ... or JSON .lic"></textarea>
</div>
<button class="btn btn-primary" onclick="issueLicense()">Sign & Issue CA Certificate</button>
<div id="issueOutput" style="margin-top: 1rem; display: none;"></div>
</div>
<div class="card">
<h3>Active & Issued Server Licenses</h3>
<table>
<thead>
<tr>
<th>License ID</th>
<th>Seats</th>
<th>Status</th>
<th>Host</th>
<th>Cert Fingerprint</th>
<th>Expires</th>
<th>Renewals</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
__ROWS_HTML__
</tbody>
</table>
</div>
</div>
<script>
async function issueLicense() {
let raw = document.getElementById('csrText').value.trim();
const seats = parseInt(document.getElementById('seats').value, 10);
let csr = raw;
let fp = "";
let host = "";
if (raw.startsWith('{')) {
try {
const parsed = JSON.parse(raw);
csr = parsed.csr;
fp = parsed.server_fingerprint || "";
host = parsed.hostname || "";
} catch(e) {
alert("Invalid JSON .lic file format");
return;
}
}
const resp = await fetch('/api/license/issue', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ csr: csr, seats: seats, server_fingerprint: fp, hostname: host })
});
const data = await resp.json();
const out = document.getElementById('issueOutput');
out.style.display = 'block';
if (resp.ok) {
out.innerHTML = `
<div style="background: #064e3b; padding: 1rem; border-radius: 4px; border: 1px solid #059669;">
<h4 style="margin: 0 0 0.5rem 0; color: #34d399;">License Issued Successfully! (Seats: ${data.seats})</h4>
<p style="margin: 0; font-size: 0.85rem;">License ID: <code>${data.license_id}</code></p>
<p style="margin: 0.5rem 0 0 0; font-size: 0.85rem;">Copy the signed CA certificate below or download it as <code>ca.crt</code>:</p>
<textarea rows="6" readonly style="margin-top: 0.5rem;">${data.ca_cert}</textarea>
</div>
`;
} else {
out.innerHTML = `<div style="background: #7f1d1d; padding: 1rem; border-radius: 4px;">Error: ${data.detail}</div>`;
}
}
async function revokeLic(licId) {
if (!confirm(`Are you sure you want to revoke license ${licId}?`)) return;
const resp = await fetch('/api/license/revoke', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ license_id: licId })
});
if (resp.ok) {
location.reload();
} else {
alert("Failed to revoke license");
}
}
</script>
</body>
</html>"""
return html_tpl.replace("__ROWS_HTML__", rows_html)
def main():
parser = argparse.ArgumentParser(description="LOGAR Master License Authority (logar.eibl.tech)")
parser.add_argument("--init-master-ca", action="store_true", help="Initialize the Master Root CA")
parser.add_argument("--sign-lic", type=str, help="Path to .lic file or CSR PEM file to sign")
parser.add_argument("--seats", type=int, default=25, help="Number of seats to grant (default: 25)")
parser.add_argument("--out", type=str, default="ca.crt", help="Output path for signed CA cert")
parser.add_argument("--revoke", type=str, help="License ID to revoke")
parser.add_argument("--list", action="store_true", help="List all issued licenses")
parser.add_argument("--serve", action="store_true", help="Run the FastAPI / mTLS renewal service")
parser.add_argument("--host", type=str, default="0.0.0.0", help="Service host (default: 0.0.0.0)")
parser.add_argument("--port", type=int, default=8080, help="Service port (default: 8080)")
args = parser.parse_args()
if args.init_master_ca:
generate_master_root_ca()
print(f"[+] Master Root CA initialized in {DEFAULT_MASTER_CERT_DIR}/")
return
if args.list:
lics = list_licenses()
print(f"\n{'License ID':<20} {'Seats':<8} {'Status':<10} {'Host':<20} {'Expires':<12} {'Renewals':<8}")
print("-" * 80)
for l in lics:
print(f"{l['license_id']:<20} {l['seats']:<8} {l['status']:<10} {l['hostname'] or 'N/A':<20} {l['expires_at'][:10]:<12} {l['renewal_count']:<8}")
return
if args.revoke:
if revoke_license(args.revoke):
print(f"[+] License {args.revoke} revoked successfully.")
else:
print(f"[-] License {args.revoke} not found.")
return
if args.sign_lic:
with open(args.sign_lic, "r", encoding="utf-8") as f:
content = f.read()
csr_pem = content
server_fp = ""
hostname = ""
if content.strip().startswith("{"):
data = json.loads(content)
csr_pem = data["csr"]
server_fp = data.get("server_fingerprint", "")
hostname = data.get("hostname", "")
res = issue_license_from_csr(
csr_pem=csr_pem,
seats=args.seats,
server_fingerprint=server_fp,
hostname=hostname
)
with open(args.out, "w", encoding="utf-8") as f:
f.write(res["ca_cert"])
print(f"[+] Signed CA certificate issued successfully!")
print(f" - License ID: {res['license_id']}")
print(f" - Granted Seats: {res['seats']}")
print(f" - Expires: {res['expires_at']}")
print(f" - Saved to: {args.out}")
return
if args.serve:
if not FASTAPI_AVAILABLE:
print("[-] FastAPI/uvicorn not installed.")
sys.exit(1)
generate_master_root_ca()
print(f"[*] Starting LOGAR License Authority at http://{args.host}:{args.port}")
uvicorn.run(app, host=args.host, port=args.port)
return
parser.print_help()
# Export public symbols from submodule into this module's namespace
for attr in dir(_sub):
if not attr.startswith("__"):
globals()[attr] = getattr(_sub, attr)
if __name__ == "__main__": if __name__ == "__main__":
main() _sub.main()
+3 -1
View File
@@ -9,7 +9,7 @@ import subprocess
import urllib.request import urllib.request
import warnings import warnings
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any, List from typing import Optional
# Suppress cryptography / pgpy deprecation notices # Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
@@ -334,6 +334,8 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
hub_url = None hub_url = None
if enrollment_secret: if enrollment_secret:
hermes_host = config.get("hermes_host", server_host) hermes_host = config.get("hermes_host", server_host)
if hermes_host in ("0.0.0.0", "::", None):
hermes_host = server_host if server_host not in ("0.0.0.0", "::", None) else "127.0.0.1"
hermes_port = config.get("hermes_port", 8443) hermes_port = config.get("hermes_port", 8443)
hub_url = f"http://{hermes_host}:{hermes_port}" hub_url = f"http://{hermes_host}:{hermes_port}"
try: try:
+3042 -1743
View File
File diff suppressed because it is too large Load Diff
+87 -4
View File
@@ -8,7 +8,7 @@ import argparse
import urllib.request import urllib.request
import warnings import warnings
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any, List from typing import Optional
# Suppress cryptography / pgpy deprecation notices # Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
@@ -115,10 +115,80 @@ def get_tls_socket(hub_host: str, hub_port: int, cert_dir: str):
return ctx.wrap_socket(raw_sock, server_hostname=hub_host) return ctx.wrap_socket(raw_sock, server_hostname=hub_host)
def is_path_writable(filepath: str) -> bool:
"""Checks if a file or directory path is actually writable by the current process."""
try:
if os.path.exists(filepath):
if os.path.isdir(filepath):
test_f = os.path.join(filepath, f".perm_{os.getpid()}")
with open(test_f, "w") as f:
f.write("1")
os.remove(test_f)
return True
else:
with open(filepath, "a") as f:
pass
return True
else:
parent = os.path.dirname(os.path.abspath(filepath)) or "."
os.makedirs(parent, exist_ok=True)
test_f = os.path.join(parent, f".perm_{os.getpid()}")
with open(test_f, "w") as f:
f.write("1")
os.remove(test_f)
return True
except Exception:
return False
def get_default_data_dir(app_name: str = "LOGAR") -> str:
"""Returns a suitable writable application data directory."""
if sys.platform == "win32":
# 1. ProgramData (C:\ProgramData\LOGAR)
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
target_prog = os.path.join(prog_data, app_name)
if is_path_writable(target_prog):
return target_prog
# 2. LocalAppData (C:\Users\<user>\AppData\Local\LOGAR)
local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
target_local = os.path.join(local_app, app_name)
if is_path_writable(target_local):
return target_local
# 3. Temp directory (%TEMP%\LOGAR)
temp_dir = os.path.join(os.environ.get("TEMP", r"C:\Windows\Temp"), app_name)
if is_path_writable(temp_dir):
return temp_dir
cwd = os.getcwd()
if is_path_writable(cwd):
return cwd
return os.path.expanduser("~")
def resolve_writable_path(path: str, app_name: str = "LOGAR", default_filename: str = "client_state.json") -> str:
"""Resolves a file or directory path, ensuring it is writable or falling back to a writable data directory."""
if not path:
path = default_filename
if os.path.isabs(path) and is_path_writable(path):
return path
if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()):
return os.path.abspath(path)
data_dir = get_default_data_dir(app_name)
fname = os.path.basename(path)
return os.path.join(data_dir, fname)
def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str: def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str:
if custom_state_path: if custom_state_path:
return custom_state_path return custom_state_path
config_dir = os.path.dirname(os.path.abspath(config_path)) config_dir = os.path.dirname(os.path.abspath(config_path))
if sys.platform == "win32" and ("Program Files" in config_dir or not is_path_writable(config_dir)):
return os.path.join(get_default_data_dir("LOGAR"), STATE_FILE_NAME)
return os.path.join(config_dir, STATE_FILE_NAME) return os.path.join(config_dir, STATE_FILE_NAME)
@@ -135,6 +205,7 @@ def load_state(state_path: str) -> dict:
def save_state(state_path: str, state: dict): def save_state(state_path: str, state: dict):
try: try:
os.makedirs(os.path.dirname(os.path.abspath(state_path)), exist_ok=True)
temp_path = f"{state_path}.tmp" temp_path = f"{state_path}.tmp"
with open(temp_path, "w", encoding="utf-8") as f: with open(temp_path, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2) json.dump(state, f, indent=2)
@@ -154,13 +225,23 @@ def commit_state(state: dict, state_path: str):
def load_config(config_path: str = CONFIG_FILE_NAME): def load_config(config_path: str = CONFIG_FILE_NAME):
if not os.path.exists(config_path): actual_config_path = config_path
if not os.path.exists(actual_config_path):
data_dir_cfg = os.path.join(get_default_data_dir("LOGAR"), os.path.basename(config_path))
if os.path.exists(data_dir_cfg):
actual_config_path = data_dir_cfg
if not os.path.exists(actual_config_path):
raise FileNotFoundError( raise FileNotFoundError(
f"Client configuration file not found at: {config_path}\n" f"Client configuration file not found at: {config_path}\n"
f"Generate one from the server using: python Server.py --create-client-config --client-out {config_path}" f"Generate one from the server using: python Server.py --create-client-config --client-out {config_path}"
) )
with open(config_path, "r", encoding="utf-8") as f: with open(actual_config_path, "r", encoding="utf-8") as f:
return json.load(f) config = json.load(f)
if "cert_dir" in config:
config["cert_dir"] = resolve_writable_path(config["cert_dir"], "LOGAR", "certs")
return config
def get_machine_identifier() -> str: def get_machine_identifier() -> str:
@@ -301,6 +382,8 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
hub_url = None hub_url = None
if enrollment_secret: if enrollment_secret:
hermes_host = config.get("hermes_host", server_host) hermes_host = config.get("hermes_host", server_host)
if hermes_host in ("0.0.0.0", "::", None):
hermes_host = server_host if server_host not in ("0.0.0.0", "::", None) else "127.0.0.1"
hermes_port = config.get("hermes_port", 8443) hermes_port = config.get("hermes_port", 8443)
hub_url = f"http://{hermes_host}:{hermes_port}" hub_url = f"http://{hermes_host}:{hermes_port}"
try: try:
+269 -4
View File
@@ -654,16 +654,263 @@ def sign_ca_csr_with_seats(
return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8") return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
def enroll_and_sync_license_ws(
cert_dir: str = "certs",
license_id: Optional[str] = None,
license_authority_ws_url: str = "wss://logar.eibl.tech/ws/license",
server_fingerprint: str = "",
hostname: str = "",
comm_cert_pem: Optional[str] = None,
master_root_ca_pem: Optional[str] = None,
seats_taken: Optional[int] = None,
timeout: int = 15,
verify_tls: bool = True
) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
"""
Connects to the Master License Authority via WebSocket protocol, authenticating and
submitting the server Sub-CA CSR to receive a cryptographically signed Sub-CA license
certificate with embedded seat quotas.
"""
os.makedirs(cert_dir, exist_ok=True)
ca_key_path = os.path.join(cert_dir, "ca.key")
ca_cert_path = os.path.join(cert_dir, "ca.crt")
# 1. Load or generate client Sub-CA private key
if os.path.exists(ca_key_path):
with open(ca_key_path, "r", encoding="utf-8") as f:
ca_key_pem = f.read()
ca_key = serialization.load_pem_private_key(ca_key_pem.encode("utf-8"), password=None)
else:
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
ca_key_pem = ca_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
).decode("utf-8")
with open(ca_key_path, "w", encoding="utf-8") as f:
f.write(ca_key_pem)
# 2. Build Sub-CA CSR
csr_common_name = f"LOGAR-Server-CA-{license_id or 'Node'}"
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "AT"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LOGAR Subordinate Node"),
x509.NameAttribute(NameOID.COMMON_NAME, csr_common_name),
])
csr = (
x509.CertificateSigningRequestBuilder()
.subject_name(subject)
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=False,
key_cert_sign=True,
crl_sign=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
.sign(ca_key, hashes.SHA256())
)
csr_pem = csr.public_bytes(serialization.Encoding.PEM).decode("utf-8")
# 3. Connect via WebSocket
try:
from websockets.sync.client import connect as ws_connect
except ImportError:
return False, "websockets library not available", None
ssl_context = None
if license_authority_ws_url.startswith("wss://"):
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
if not verify_tls:
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
elif master_root_ca_pem:
ssl_context.load_verify_locations(cadata=master_root_ca_pem)
else:
upstream_path = os.path.join(cert_dir, "upstream_ca.crt")
if os.path.exists(upstream_path):
ssl_context.load_verify_locations(cafile=upstream_path)
try:
with ws_connect(
license_authority_ws_url,
ssl=ssl_context,
open_timeout=timeout,
close_timeout=timeout
) as ws:
payload = {
"action": "enroll_or_sync",
"license_id": license_id,
"communication_cert": comm_cert_pem,
"server_csr": csr_pem,
"server_fingerprint": server_fingerprint,
"hostname": hostname
}
if seats_taken is not None:
payload["seats_taken"] = seats_taken
ws.send(json.dumps(payload))
raw_reply = ws.recv(timeout=timeout)
reply = json.loads(raw_reply)
if reply.get("status") != "success":
return False, f"WebSocket licensing error: {reply.get('message', raw_reply)}", None
new_ca_pem = reply.get("ca_cert")
if not new_ca_pem:
return False, "License response missing 'ca_cert'", None
# Backup old ca.crt if present
if os.path.exists(ca_cert_path):
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
try:
os.replace(ca_cert_path, f"{ca_cert_path}.{ts}.bak")
except Exception:
pass
with open(ca_cert_path, "w", encoding="utf-8") as f:
f.write(new_ca_pem)
upstream_ca = reply.get("master_root_ca") or master_root_ca_pem
if upstream_ca:
with open(os.path.join(cert_dir, "upstream_ca.crt"), "w", encoding="utf-8") as f:
f.write(upstream_ca)
lic_info = extract_license_info_from_cert(new_ca_pem)
return True, f"License {license_id} successfully enrolled via WebSocket ({reply.get('seats')} seats)", {
"license_id": license_id,
"seats": reply.get("seats"),
"ca_cert_pem": new_ca_pem,
"master_root_ca_pem": upstream_ca,
"license_info": lic_info
}
except Exception as e:
return False, f"WebSocket enrollment failed: {str(e)}", None
def import_license_package(
lic_content_or_path: str,
cert_dir: str = "certs",
server_fingerprint: str = "",
hostname: str = ""
) -> Dict[str, Any]:
"""
Imports and validates a license package or certificate.
Supports:
1. Bootstrap JSON .lic package (auto-enrolls via WebSocket to receive signed CA cert)
2. Activated JSON .lic package containing 'ca_cert' and optional 'master_root_ca'
3. Raw PEM Subordinate CA certificate
4. File path pointing to any of the above
Saves the signed CA certificate to cert_dir/ca.crt (backing up any old ca.crt),
and if 'master_root_ca' is present, saves it to cert_dir/upstream_ca.crt for mTLS trust.
Returns extracted license attributes and PEM contents.
"""
os.makedirs(cert_dir, exist_ok=True)
raw = lic_content_or_path
if os.path.exists(lic_content_or_path) and os.path.isfile(lic_content_or_path):
with open(lic_content_or_path, "r", encoding="utf-8") as f:
raw = f.read()
ca_pem = None
master_root_ca_pem = None
trimmed = raw.strip()
if trimmed.startswith("{"):
try:
data = json.loads(trimmed)
except Exception as e:
raise ValueError(f"Failed to parse JSON .lic package: {e}")
# If this is a bootstrap license package, auto-enroll over WebSocket
if data.get("format") == "LOGAR_BOOTSTRAP_LICENSE" or (data.get("communication_cert") and not data.get("ca_cert")):
lic_id = data.get("license_id")
ws_url = data.get("license_authority", "wss://logar.eibl.tech/ws/license")
comm_cert = data.get("communication_cert")
master_ca = data.get("master_root_ca")
if master_ca:
with open(os.path.join(cert_dir, "upstream_ca.crt"), "w", encoding="utf-8") as f:
f.write(master_ca)
ok, msg, res = enroll_and_sync_license_ws(
cert_dir=cert_dir,
license_id=lic_id,
license_authority_ws_url=ws_url,
server_fingerprint=server_fingerprint or data.get("server_fingerprint", ""),
hostname=hostname or data.get("hostname", ""),
comm_cert_pem=comm_cert,
master_root_ca_pem=master_ca
)
if not ok or not res:
raise ValueError(msg)
return res
ca_pem = data.get("ca_cert") or data.get("cert_pem")
master_root_ca_pem = data.get("master_root_ca") or data.get("upstream_ca")
elif "-----BEGIN CERTIFICATE-----" in trimmed:
ca_pem = trimmed
if not ca_pem:
raise ValueError("Provided license package does not contain a valid 'ca_cert' PEM certificate.")
info = extract_license_info_from_cert(ca_pem)
if not info.get("valid"):
raise ValueError(f"Invalid CA certificate in license: {info.get('error')}")
# Backup existing ca.crt if present
ca_path = os.path.join(cert_dir, "ca.crt")
if os.path.exists(ca_path):
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
try:
os.replace(ca_path, f"{ca_path}.{ts}.bak")
except Exception:
pass
with open(ca_path, "w", encoding="utf-8") as f:
f.write(ca_pem)
# Save upstream_ca.crt if master_root_ca is present
if master_root_ca_pem:
upstream_path = os.path.join(cert_dir, "upstream_ca.crt")
with open(upstream_path, "w", encoding="utf-8") as f:
f.write(master_root_ca_pem)
return {
"ca_cert_pem": ca_pem,
"master_root_ca_pem": master_root_ca_pem,
"license_info": info,
"seats": info.get("seats"),
"license_id": info.get("license_id"),
"issuer_url": info.get("issuer_url"),
"fingerprint": info.get("fingerprint"),
"expires_at": info.get("not_valid_after")
}
def renew_ca_cert_mtls( def renew_ca_cert_mtls(
cert_dir: str = "certs", cert_dir: str = "certs",
license_server_url: str = "https://logar.eibl.tech", license_server_url: str = "https://logar.eibl.tech",
upstream_ca_pem: Optional[str] = None, upstream_ca_pem: Optional[str] = None,
requested_seats: Optional[int] = None,
seat_increment: int = 0,
seats_taken: Optional[int] = None,
active_seats: Optional[int] = None,
hostname: Optional[str] = None,
server_fingerprint: Optional[str] = None,
timeout: int = 15, timeout: int = 15,
verify_tls: bool = True verify_tls: bool = True
) -> Tuple[bool, str, Optional[str]]: ) -> Tuple[bool, str, Optional[str]]:
""" """
Connects to the master License Authority using mutual TLS (mTLS), authenticating Connects to the master License Authority using mutual TLS (mTLS), authenticating
with the server's current ca.crt and ca.key, and requests an automated renewal. with the server's current ca.crt and ca.key, and requests an automated renewal.
Supports requesting seat quota increases or increments during renewal and reporting active seat usage.
If renewed, writes backup of previous ca.crt and saves new ca.crt in cert_dir. If renewed, writes backup of previous ca.crt and saves new ca.crt in cert_dir.
Returns (success, message, new_ca_cert_pem). Returns (success, message, new_ca_cert_pem).
""" """
@@ -681,11 +928,22 @@ def renew_ca_cert_mtls(
current_pem = f.read() current_pem = f.read()
info = extract_license_info_from_cert(current_pem) info = extract_license_info_from_cert(current_pem)
payload = json.dumps({ effective_seats_taken = seats_taken if seats_taken is not None else active_seats
payload_dict = {
"license_id": info.get("license_id"), "license_id": info.get("license_id"),
"fingerprint": info.get("fingerprint"), "fingerprint": info.get("fingerprint"),
"current_cert": current_pem "current_cert": current_pem,
}).encode("utf-8") "seats_taken": effective_seats_taken,
"active_seats": effective_seats_taken,
"hostname": hostname or "",
"server_fingerprint": server_fingerprint or ""
}
if requested_seats is not None:
payload_dict["requested_seats"] = requested_seats
if seat_increment:
payload_dict["seat_increment"] = seat_increment
payload = json.dumps(payload_dict).encode("utf-8")
path = parsed.path or "/api/license/renew" path = parsed.path or "/api/license/renew"
headers = { headers = {
@@ -699,7 +957,7 @@ def renew_ca_cert_mtls(
ssl_ctx.check_hostname = False ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE ssl_ctx.verify_mode = ssl.CERT_NONE
elif upstream_ca_pem: elif upstream_ca_pem:
ssl_ctx.load_verify_locations(cdata=upstream_ca_pem) ssl_ctx.load_verify_locations(cadata=upstream_ca_pem)
upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt") upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt")
if os.path.exists(upstream_ca_path) and verify_tls and not upstream_ca_pem: if os.path.exists(upstream_ca_path) and verify_tls and not upstream_ca_pem:
ssl_ctx.load_verify_locations(cafile=upstream_ca_path) ssl_ctx.load_verify_locations(cafile=upstream_ca_path)
@@ -730,6 +988,13 @@ def renew_ca_cert_mtls(
with open(ca_cert_path, "w", encoding="utf-8") as f: with open(ca_cert_path, "w", encoding="utf-8") as f:
f.write(new_cert_pem) f.write(new_cert_pem)
# Update upstream_ca.crt if master root CA certificate was provided
master_root_pem = result.get("master_root_ca")
if master_root_pem:
upstream_path = os.path.join(cert_dir, "upstream_ca.crt")
with open(upstream_path, "w", encoding="utf-8") as f:
f.write(master_root_pem)
return True, "Certificate renewed successfully via mTLS", new_cert_pem return True, "Certificate renewed successfully via mTLS", new_cert_pem
except Exception as e: except Exception as e:
+4
View File
@@ -0,0 +1,4 @@
import warnings
# Suppress cryptography / pgpy deprecation notices during test runs
warnings.filterwarnings("ignore")
+92 -88
View File
@@ -6,6 +6,11 @@ import sqlite3
import unittest import unittest
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Ensure src/ is importable # Ensure src/ is importable
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SRC_DIR = os.path.join(ROOT_DIR, "src") SRC_DIR = os.path.join(ROOT_DIR, "src")
@@ -15,32 +20,68 @@ if SRC_DIR not in sys.path:
sys.path.insert(0, SRC_DIR) sys.path.insert(0, SRC_DIR)
import server_enrollment as enrollment import server_enrollment as enrollment
import License_Authority as authority
import Server import Server
class TestLicenseAuthorityAndWebUI(unittest.TestCase): def _create_mock_signed_license_cert(csr_pem: str, seats: int = 50, license_id: str = "LIC-TEST-12345") -> tuple:
"""
Helper to generate a mock master CA and sign a sub-CA CSR with LOGAR license OIDs
using pure cryptography library (without importing the private License_Authority submodule).
"""
# 1. Generate Root CA
ca_key = rsa.generate_private_key(65537, 2048)
ca_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "AT"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LOGAR Master Authority"),
x509.NameAttribute(NameOID.COMMON_NAME, "LOGAR-Master-Root-CA")
])
now = datetime.now(timezone.utc)
ca_cert = (
x509.CertificateBuilder()
.subject_name(ca_name)
.issuer_name(ca_name)
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(days=1))
.not_valid_after(now + timedelta(days=3650))
.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
.sign(ca_key, hashes.SHA256())
)
ca_pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
# 2. Parse CSR and sign Subordinate CA with custom OIDs
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
sub_cert = (
x509.CertificateBuilder()
.subject_name(csr.subject)
.issuer_name(ca_name)
.public_key(csr.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(hours=1))
.not_valid_after(now + timedelta(days=365))
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_SEATS, str(seats).encode("utf-8")), critical=False)
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_LICENSE_ID, license_id.encode("utf-8")), critical=False)
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_ISSUER_URL, b"https://logar.eibl.tech"), critical=False)
.sign(ca_key, hashes.SHA256())
)
sub_pem = sub_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
return sub_pem, ca_pem
class TestServerLicensingAndWebUI(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp") self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp")
os.makedirs(self.test_dir, exist_ok=True) os.makedirs(self.test_dir, exist_ok=True)
self.master_ca_dir = os.path.join(self.test_dir, "master_ca")
self.server_cert_dir = os.path.join(self.test_dir, "server_certs") self.server_cert_dir = os.path.join(self.test_dir, "server_certs")
self.db_path = os.path.join(self.test_dir, "license_auth.db")
self.server_db = os.path.join(self.test_dir, "server_state.db") self.server_db = os.path.join(self.test_dir, "server_state.db")
def tearDown(self): def tearDown(self):
if os.path.exists(self.test_dir): if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir, ignore_errors=True) shutil.rmtree(self.test_dir, ignore_errors=True)
def test_master_root_ca_generation(self):
"""Verifies Master Root CA creation and structure."""
cert_pem, key_pem = authority.generate_master_root_ca(cert_dir=self.master_ca_dir)
self.assertIn("BEGIN CERTIFICATE", cert_pem)
self.assertIn("BEGIN RSA PRIVATE KEY", key_pem)
self.assertTrue(os.path.exists(os.path.join(self.master_ca_dir, "master_root_ca.crt")))
def test_ca_csr_and_lic_request_generation(self): def test_ca_csr_and_lic_request_generation(self):
"""Verifies subordinate CA CSR and .lic request file generation.""" """Verifies subordinate CA CSR and .lic request file generation on the server."""
lic_data, lic_json = enrollment.create_license_request_lic( lic_data, lic_json = enrollment.create_license_request_lic(
cert_dir=self.server_cert_dir, cert_dir=self.server_cert_dir,
license_server="https://logar.eibl.tech", license_server="https://logar.eibl.tech",
@@ -55,88 +96,26 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.csr"))) self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.csr")))
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "logar_server.lic"))) self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "logar_server.lic")))
def test_authority_issue_sub_ca_with_exact_seats(self): def test_license_info_extraction_and_custom_oids(self):
"""Verifies that License Authority signs CSR, embeds exact seat quota via custom OID, and verifies in DB.""" """Verifies extraction of embedded custom seat OIDs and license attributes from certificates."""
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir) csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
result = authority.issue_license_from_csr( sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=42, license_id="LIC-ABC-4242")
csr_pem=csr_pem,
seats=42,
days_valid=365,
server_fingerprint="FP123456",
hostname="hub-node-1",
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
self.assertEqual(result["status"], "success") info = enrollment.extract_license_info_from_cert(sub_pem)
self.assertEqual(result["seats"], 42)
self.assertIn("BEGIN CERTIFICATE", result["ca_cert"])
# Decode certificate to verify seat quota extraction
info = enrollment.extract_license_info_from_cert(result["ca_cert"])
self.assertTrue(info["valid"]) self.assertTrue(info["valid"])
self.assertEqual(info["seats"], 42) self.assertEqual(info["seats"], 42)
self.assertEqual(info["license_id"], result["license_id"]) self.assertEqual(info["license_id"], "LIC-ABC-4242")
self.assertEqual(info["issuer_url"], "https://logar.eibl.tech") self.assertEqual(info["issuer_url"], "https://logar.eibl.tech")
self.assertFalse(info["is_expired"]) self.assertFalse(info["is_expired"])
self.assertTrue(info["is_ca"]) self.assertTrue(info["is_ca"])
# Check authority database
lics = authority.list_licenses(db_path=self.db_path)
self.assertEqual(len(lics), 1)
self.assertEqual(lics[0]["license_id"], result["license_id"])
self.assertEqual(lics[0]["seats"], 42)
self.assertEqual(lics[0]["status"], "active")
def test_authority_renewal_and_revocation(self):
"""Verifies that an active license cert can be renewed, and revoked licenses are rejected."""
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
initial = authority.issue_license_from_csr(
csr_pem=csr_pem,
seats=15,
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
lic_id = initial["license_id"]
# Successful renewal
renewed = authority.renew_license_cert(
current_cert_pem=initial["ca_cert"],
days_valid=365,
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
self.assertEqual(renewed["status"], "renewed")
self.assertEqual(renewed["seats"], 15)
# Check renewal count in DB
lics = authority.list_licenses(db_path=self.db_path)
self.assertEqual(lics[0]["renewal_count"], 1)
# Revoke the license
revoked = authority.revoke_license(lic_id, db_path=self.db_path)
self.assertTrue(revoked)
# Renewal attempt after revocation must fail
with self.assertRaises(ValueError):
authority.renew_license_cert(
current_cert_pem=renewed["ca_cert"],
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
def test_server_license_import_and_seat_update(self): def test_server_license_import_and_seat_update(self):
"""Verifies that importing a signed CA cert updates the server's seat quota in DB and memory.""" """Verifies that importing a signed CA cert updates the server's seat quota in DB and memory."""
# 1. Generate CSR on server side # 1. Generate CSR on server side
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir) csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
# 2. Authority signs it with 50 seats # 2. Mock sign it with 50 seats
signed = authority.issue_license_from_csr( sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=50, license_id="LIC-IMPORT-50")
csr_pem=csr_pem,
seats=50,
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
# 3. Setup mock server state and DB # 3. Setup mock server state and DB
Server.init_db(self.server_db, max_seats=5) Server.init_db(self.server_db, max_seats=5)
@@ -160,16 +139,16 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
# 5. Save the signed CA certificate to cert_dir/ca.crt # 5. Save the signed CA certificate to cert_dir/ca.crt
ca_path = os.path.join(self.server_cert_dir, "ca.crt") ca_path = os.path.join(self.server_cert_dir, "ca.crt")
with open(ca_path, "w", encoding="utf-8") as f: with open(ca_path, "w", encoding="utf-8") as f:
f.write(signed["ca_cert"]) f.write(sub_pem)
# 6. Extract summary after import # 6. Extract summary after import
summary_after = Server.get_license_summary() summary_after = Server.get_license_summary()
self.assertEqual(summary_after["max_seats"], 50) self.assertEqual(summary_after["max_seats"], 50)
self.assertEqual(summary_after["license_id"], signed["license_id"]) self.assertEqual(summary_after["license_id"], "LIC-IMPORT-50")
self.assertEqual(summary_after["status"], "ACTIVE") self.assertEqual(summary_after["status"], "ACTIVE")
def test_server_client_revocation_and_reactivation(self): def test_server_client_revocation_and_reactivation(self):
"""Verifies revocation and reactivation of edge client seats.""" """Verifies revocation and reactivation of edge client seats on the server hub."""
Server.init_db(self.server_db, max_seats=2) Server.init_db(self.server_db, max_seats=2)
Server.SERVER_STATE["config"] = { Server.SERVER_STATE["config"] = {
"db_path": self.server_db, "db_path": self.server_db,
@@ -202,6 +181,30 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
self.assertEqual(summary2["active_seats"], 2) self.assertEqual(summary2["active_seats"], 2)
self.assertEqual(summary2["available_seats"], 0) self.assertEqual(summary2["available_seats"], 0)
def test_activated_lic_package_handling(self):
"""Verifies that an activated .lic package structure can be processed and verified."""
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
sub_pem, root_pem = _create_mock_signed_license_cert(csr_pem, seats=20, license_id="LIC-ACT-20")
lic_package = {
"format": "LOGAR_ACTIVATED_LICENSE",
"version": "2.0.1",
"license_id": "LIC-ACT-20",
"seats": 20,
"server_fingerprint": "FP_TEST_HUB",
"ca_cert": sub_pem,
"master_root_ca": root_pem,
"days_valid": 365
}
self.assertEqual(lic_package["seats"], 20)
self.assertIn("BEGIN CERTIFICATE", lic_package["ca_cert"])
self.assertIn("BEGIN CERTIFICATE", lic_package["master_root_ca"])
# Verify certificate parses
info = enrollment.extract_license_info_from_cert(lic_package["ca_cert"])
self.assertEqual(info["seats"], 20)
self.assertEqual(info["license_id"], "LIC-ACT-20")
def test_webui_rendering(self): def test_webui_rendering(self):
"""Verifies that the WebUI HTML contains key components, stats, and download triggers.""" """Verifies that the WebUI HTML contains key components, stats, and download triggers."""
Server.init_db(self.server_db, max_seats=10) Server.init_db(self.server_db, max_seats=10)
@@ -218,10 +221,11 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
response = Server.serve_webui() response = Server.serve_webui()
html = response.body.decode("utf-8") html = response.body.decode("utf-8")
self.assertIn("LOGAR Fleet Hub &amp; License Management", html.replace("&", "&amp;")) self.assertIn("LOGAR Fleet Hub", html)
self.assertIn("Export .lic Request", html) self.assertNotIn("LOGAR Fleet Hub & License Management", html)
self.assertIn("Import &amp; Activate Signed CA Certificate", html.replace("&", "&amp;")) self.assertNotIn("Export .lic Request", html)
self.assertIn("Renew via mTLS Now", html) self.assertNotIn("Import License (.lic / ca.crt)", html)
self.assertIn("Sync & Renew via mTLS", html)
self.assertIn("logar.eibl.tech", html) self.assertIn("logar.eibl.tech", html)
+7 -1
View File
@@ -6,6 +6,7 @@ import unittest
import warnings import warnings
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
warnings.simplefilter("ignore")
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
@@ -26,7 +27,12 @@ class TestLinuxClientComponent(unittest.TestCase):
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage}, usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
hashes=[HashAlgorithm.SHA256], hashes=[HashAlgorithm.SHA256],
ciphers=[SymmetricKeyAlgorithm.AES256], ciphers=[SymmetricKeyAlgorithm.AES256],
compression=[CompressionAlgorithm.Uncompressed] compression=[
CompressionAlgorithm.ZIP,
CompressionAlgorithm.ZLIB,
CompressionAlgorithm.BZ2,
CompressionAlgorithm.Uncompressed
]
) )
self.server_priv = key self.server_priv = key
self.server_pub = key.pubkey self.server_pub = key.pubkey
+13 -9
View File
@@ -109,7 +109,9 @@ def run_tests():
print(f"[OK] Client enrolled successfully. Certificates stored in {cert_dir}") print(f"[OK] Client enrolled successfully. Certificates stored in {cert_dir}")
# 2c. Verify client shows in /api/clients # 2c. Verify client shows in /api/clients
clients_req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/clients") auth_tok = server_conf.get("auth_token") or client_conf.get("auth_token")
client_headers = {"Authorization": f"Bearer {auth_tok}"} if auth_tok else {}
clients_req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/clients", headers=client_headers)
with urllib.request.urlopen(clients_req, timeout=5) as resp: with urllib.request.urlopen(clients_req, timeout=5) as resp:
clients_data = json.loads(resp.read().decode("utf-8")) clients_data = json.loads(resp.read().decode("utf-8"))
assert clients_data["active_seats"] >= 1 assert clients_data["active_seats"] >= 1
@@ -146,8 +148,8 @@ def run_tests():
resp_bytes.extend(chunk) resp_bytes.extend(chunk)
return json.loads(resp_bytes.decode("utf-8")) return json.loads(resp_bytes.decode("utf-8"))
print("\n=== [3] Testing Temporal Persistence & 4-Run Rule for Warnings over mTLS ===") print("\n=== [3] Testing Temporal Persistence & Direct Anomaly Verification over mTLS ===")
test_signature = "TestServiceDegraded" test_signature = f"TestServiceDegraded_{int(time.time())}"
candidate_log = [{ candidate_log = [{
"server": client_id, "server": client_id,
"os_type": "linux", "os_type": "linux",
@@ -161,10 +163,10 @@ def run_tests():
assert resp.get("status") == "success", f"Run {run_num} failed: {resp}" assert resp.get("status") == "success", f"Run {run_num} failed: {resp}"
promoted = resp.get("promoted_verified", 0) promoted = resp.get("promoted_verified", 0)
print(f"[Run {run_num}/4] Ingested successfully via mTLS. Promoted to verified: {promoted}") print(f"[Run {run_num}/4] Ingested successfully via mTLS. Promoted to verified: {promoted}")
if run_num < 4: if run_num == 1:
assert promoted == 0, f"Expected 0 promoted on run {run_num} for warning, got {promoted}" assert promoted == 1, f"Expected 1 promoted on run 1 for warning, got {promoted}"
else: else:
assert promoted == 1, f"Expected 1 promoted on run 4 for warning, got {promoted}" assert promoted == 0, f"Expected 0 newly promoted on run {run_num} (already verified), got {promoted}"
# Inspect SQLite database directly # Inspect SQLite database directly
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db")) conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
@@ -178,10 +180,10 @@ def run_tests():
print(f"[DB Verification] Issue '{test_signature}' -> run_count: {run_count}, status: {status}") print(f"[DB Verification] Issue '{test_signature}' -> run_count: {run_count}, status: {status}")
assert run_count >= 4, f"Expected run_count >= 4, got {run_count}" assert run_count >= 4, f"Expected run_count >= 4, got {run_count}"
assert status == "VERIFIED", f"Expected status 'VERIFIED', got {status}" assert status == "VERIFIED", f"Expected status 'VERIFIED', got {status}"
print("[OK] 4-Run Rule verified: Warning promoted to VERIFIED anomaly on 4th run over mTLS!") print("[OK] Direct verification confirmed: Warning immediately verified for Hermes with run count tracking over mTLS!")
print("\n=== [4] Testing Immediate Pass for Errors over mTLS ===") print("\n=== [4] Testing Immediate Pass for Errors over mTLS ===")
error_signature = "TestServiceCrashImmediate" error_signature = f"TestServiceCrashImmediate_{int(time.time())}"
error_log = [{ error_log = [{
"server": client_id, "server": client_id,
"os_type": "linux", "os_type": "linux",
@@ -207,7 +209,9 @@ def run_tests():
print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!") print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!")
print("\n=== [5] Testing Hermes Reporting Endpoint (/api/hermes/report) ===") print("\n=== [5] Testing Hermes Reporting Endpoint (/api/hermes/report) ===")
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report") token = server_conf.get("auth_token") or client_conf.get("auth_token")
headers = {"Authorization": f"Bearer {token}"} if token else {}
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report", headers=headers)
with urllib.request.urlopen(req, timeout=5) as response: with urllib.request.urlopen(req, timeout=5) as response:
assert response.status == 200, f"Expected 200, got {response.status}" assert response.status == 200, f"Expected 200, got {response.status}"
hermes_data = json.loads(response.read().decode("utf-8")) hermes_data = json.loads(response.read().decode("utf-8"))
+130 -16
View File
@@ -75,7 +75,7 @@ class TestServerComponent(unittest.TestCase):
if os.path.exists(client_out): if os.path.exists(client_out):
os.remove(client_out) os.remove(client_out)
def test_4_run_rule_and_12h_window(self): def test_direct_hermes_anomaly_and_12h_window(self):
Server.init_db(self.test_db) Server.init_db(self.test_db)
log_entry = { log_entry = {
"server": "app-worker-01.corp.local", "server": "app-worker-01.corp.local",
@@ -89,23 +89,24 @@ class TestServerComponent(unittest.TestCase):
"logs": [log_entry] "logs": [log_entry]
} }
# Runs 1 to 3: WARNING should remain TRANSIENT # Run 1: WARNING is immediately VERIFIED for Hermes AI reasoning
for run_idx in range(1, 4): res1 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4) self.assertEqual(res1["status"], "success")
self.assertEqual(res["status"], "success") self.assertEqual(res1["promoted_verified"], 1)
self.assertEqual(res["promoted_verified"], 0)
conn = sqlite3.connect(self.test_db) conn = sqlite3.connect(self.test_db)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",)) c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
row = c.fetchone() row = c.fetchone()
conn.close() conn.close()
self.assertEqual(row[0], 3) self.assertEqual(row[0], 1)
self.assertEqual(row[1], "TRANSIENT") self.assertEqual(row[1], "VERIFIED")
# Run 4: promotes WARNING to VERIFIED! # Runs 2 to 4: Increments consecutive run count while remaining VERIFIED
res4 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4) for run_idx in range(2, 5):
self.assertEqual(res4["promoted_verified"], 1) res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
self.assertEqual(res["status"], "success")
self.assertEqual(res["promoted_verified"], 0)
conn = sqlite3.connect(self.test_db) conn = sqlite3.connect(self.test_db)
c = conn.cursor() c = conn.cursor()
@@ -130,7 +131,7 @@ class TestServerComponent(unittest.TestCase):
} }
# Run 1: ERROR must immediately promote to VERIFIED # Run 1: ERROR must immediately promote to VERIFIED
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4) res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
self.assertEqual(res["promoted_verified"], 1) self.assertEqual(res["promoted_verified"], 1)
@@ -156,7 +157,7 @@ class TestServerComponent(unittest.TestCase):
{"server": "app-worker-01", "signature": "SigTrace", "severity": "TRACE", "message": "Trace msg", "os_type": "linux"} {"server": "app-worker-01", "signature": "SigTrace", "severity": "TRACE", "message": "Trace msg", "os_type": "linux"}
] ]
} }
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4) res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
conn = sqlite3.connect(self.test_db) conn = sqlite3.connect(self.test_db)
@@ -171,10 +172,10 @@ class TestServerComponent(unittest.TestCase):
self.assertNotIn("SigDebug", rows) self.assertNotIn("SigDebug", rows)
self.assertNotIn("SigTrace", rows) self.assertNotIn("SigTrace", rows)
# SigErr is immediately VERIFIED; SigWarn and SigInfo are TRANSIENT on run 1 # All actionable issues (ERROR, WARNING, INFO) are promoted to VERIFIED immediately for Hermes reasoning
self.assertEqual(rows["SigErr"], "VERIFIED") self.assertEqual(rows["SigErr"], "VERIFIED")
self.assertEqual(rows["SigWarn"], "TRANSIENT") self.assertEqual(rows["SigWarn"], "VERIFIED")
self.assertEqual(rows["SigInfo"], "TRANSIENT") self.assertEqual(rows["SigInfo"], "VERIFIED")
def test_license_schema_and_pki_generation(self): def test_license_schema_and_pki_generation(self):
secret = "test-secret-12345" secret = "test-secret-12345"
@@ -328,6 +329,119 @@ class TestServerComponent(unittest.TestCase):
if os.path.exists(test_cert_dir): if os.path.exists(test_cert_dir):
shutil.rmtree(test_cert_dir, ignore_errors=True) shutil.rmtree(test_cert_dir, ignore_errors=True)
def test_user_auth_and_session(self):
"""Tests admin password hashing, verification, session tokens, and password change."""
Server.init_db(self.test_db)
# Verify default admin credentials
self.assertTrue(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
self.assertFalse(Server.verify_user_credentials("admin", "wrong_password", db_path=self.test_db))
self.assertFalse(Server.verify_user_credentials("nonexistent", "password", db_path=self.test_db))
# Test session token generation & validation
token = Server.generate_session_token("admin")
self.assertEqual(Server.verify_session_token(token), "admin")
self.assertIsNone(Server.verify_session_token("invalid.token.structure"))
# Test changing password
Server.change_user_password("admin", "new_secure_pass123", db_path=self.test_db)
self.assertFalse(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
self.assertTrue(Server.verify_user_credentials("admin", "new_secure_pass123", db_path=self.test_db))
def test_hermes_tokens_and_auth(self):
"""Tests Hermes Bearer token creation, verification, revocation, and deletion."""
Server.init_db(self.test_db)
# Test default Hermes token was created on init
tokens = Server.list_hermes_tokens(db_path=self.test_db)
self.assertEqual(len(tokens), 1)
self.assertEqual(tokens[0]["status"], "active")
default_token = tokens[0]["token"]
self.assertTrue(Server.verify_hermes_token(default_token, db_path=self.test_db))
# Test creating new token
new_tok = Server.create_hermes_token(name="Hermes Autonomous Agent 01", db_path=self.test_db)
self.assertIn("token", new_tok)
self.assertEqual(new_tok["name"], "Hermes Autonomous Agent 01")
self.assertTrue(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
# Test revoking token
self.assertTrue(Server.revoke_hermes_token(new_tok["token"], db_path=self.test_db))
self.assertFalse(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
# Test deleting token
self.assertTrue(Server.delete_hermes_token(new_tok["token"], db_path=self.test_db))
tokens_after = Server.list_hermes_tokens(db_path=self.test_db)
self.assertEqual(len(tokens_after), 1)
def test_client_profile_export(self):
"""Tests client profile dictionary generation and export formatting."""
cfg = Server.load_or_init_config(self.test_config)
profile = Server.generate_client_profile_dict(
server_host="logar-hub.corp.internal",
server_port=9443,
client_id="worker-node-101",
config_dict=cfg
)
self.assertEqual(profile["server_host"], "logar-hub.corp.internal")
self.assertEqual(profile["server_port"], 9443)
self.assertEqual(profile["client_id"], "worker-node-101")
self.assertEqual(profile["server_fingerprint"], cfg["server_fingerprint"])
self.assertEqual(profile["server_public_key"], cfg["public_key"])
self.assertEqual(profile["enrollment_secret"], cfg["enrollment_secret"])
self.assertEqual(profile["format"], "LOGAR_CLIENT_PROFILE")
def test_client_seat_management_and_relative_time(self):
"""Tests client listing, seat approval, revocation, deletion, and relative timestamp helper."""
Server.init_db(self.test_db, enrollment_secret="test-sec-123", max_seats=5)
Server.SERVER_STATE["config"] = {"db_path": self.test_db}
# Populate test clients
conn = sqlite3.connect(self.test_db)
now = datetime.now(timezone.utc)
five_min_ago = (now - timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S")
two_hours_ago = (now - timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S")
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-win', 'WIN-PROD-01', 'windows', 'FP_W1', 'active', ?)", (five_min_ago,))
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-lin', 'LIN-PROD-01', 'linux', 'FP_L1', 'active', ?)", (two_hours_ago,))
conn.commit()
conn.close()
# 1. Test listing
client_data = Server.list_clients()
self.assertEqual(client_data["active_seats"], 2)
self.assertEqual(client_data["max_seats"], 5)
self.assertEqual(len(client_data["clients"]), 2)
self.assertEqual(client_data["clients"][0]["last_activity_relative"], "5m ago")
# 2. Test revoking
rev_req = Server.ClientActionRequest(client_id="c-win")
rev_res = Server.revoke_client(rev_req)
self.assertEqual(rev_res["status"], "revoked")
client_data = Server.list_clients()
self.assertEqual(client_data["active_seats"], 1)
# 3. Test approving / reactivating
app_res = Server.approve_client(rev_req)
self.assertEqual(app_res["status"], "active")
client_data = Server.list_clients()
self.assertEqual(client_data["active_seats"], 2)
# 4. Test deleting client record
del_res = Server.delete_client(rev_req)
self.assertEqual(del_res["status"], "deleted")
client_data = Server.list_clients()
self.assertEqual(len(client_data["clients"]), 1)
self.assertEqual(client_data["active_seats"], 1)
# 5. Test format_relative_time edge cases
self.assertEqual(Server.format_relative_time(None), "Never")
self.assertEqual(Server.format_relative_time(""), "Never")
self.assertEqual(Server.format_relative_time("invalid-date-string"), "invalid-date-str")
just_now = now.strftime("%Y-%m-%d %H:%M:%S")
self.assertIn(Server.format_relative_time(just_now), ("0s ago", "1s ago", "Just now"))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+7 -1
View File
@@ -6,6 +6,7 @@ import unittest
import warnings import warnings
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
warnings.simplefilter("ignore")
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
@@ -26,7 +27,12 @@ class TestWinClientComponent(unittest.TestCase):
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage}, usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
hashes=[HashAlgorithm.SHA256], hashes=[HashAlgorithm.SHA256],
ciphers=[SymmetricKeyAlgorithm.AES256], ciphers=[SymmetricKeyAlgorithm.AES256],
compression=[CompressionAlgorithm.Uncompressed] compression=[
CompressionAlgorithm.ZIP,
CompressionAlgorithm.ZLIB,
CompressionAlgorithm.BZ2,
CompressionAlgorithm.Uncompressed
]
) )
self.server_priv = key self.server_priv = key
self.server_pub = key.pubkey self.server_pub = key.pubkey