223 Commits
Author SHA1 Message Date
me0nline 900d03b7f7 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m37s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 5m4s
2026-09-27 17:10:29 +02:00
me0nline a8d59b2f14 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m14s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m15s
2026-09-27 17:00:17 +02:00
me0nline b79022bc41 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m17s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m15s
2026-09-27 16:07:18 +02:00
me0nline fcba813edd windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m26s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m17s
2026-09-27 15:51:03 +02:00
me0nline 72e3527c79 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m29s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m18s
2026-09-27 15:06:32 +02:00
me0nline 4a40b4d395 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m41s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m47s
2026-09-27 15:00:26 +02:00
me0nline 399cf469c5 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m19s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 9m48s
2026-09-26 22:30:43 +02:00
me0nline ce7d2b97d2 windows installer fixes
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 7m43s
2026-09-26 22:09:40 +02:00
me0nline 2e256baf4a windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m13s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 6m51s
2026-09-26 22:00:50 +02:00
me0nline 1a984caaf7 windows installer fixes
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 4m42s
2026-09-26 21:42:51 +02:00
me0nline 96756ca5db windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m14s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 4m29s
2026-09-26 21:34:21 +02:00
me0nline 157229a84e windows installer fixes
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 10m27s
2026-09-26 21:21:36 +02:00
me0nline 8287af3129 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m36s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 10m7s
2026-09-26 21:07:34 +02:00
me0nline a133050cb5 windows installer fixes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m13s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 7m32s
2026-09-26 20:59:13 +02:00
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
me0nline d331da0b13 docs(linux_client): note fleet seat quota accounting and socket revocation behavior
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 17s
2026-09-24 16:50:53 +02:00
me0nline 378127c25b docs(win_client): note fleet seat quota accounting and socket revocation behavior 2026-09-24 16:50:23 +02:00
me0nline 4970ef7c25 docs(linux_server): document Master License Authority, WebUI dashboard, and .lic export 2026-09-24 16:50:08 +02:00
me0nline 79a78ba1c1 docs(win_server): document Master License Authority, WebUI dashboard, and .lic export 2026-09-24 16:49:22 +02:00
me0nline 14533e0427 docs: update root README with Master License Authority, mTLS auto-renewal, and WebUI dashboard 2026-09-24 16:48:54 +02:00
me0nline d192b43aaf test(licensing): add tests for Master License Authority, X.509 seat quotas, and WebUI 2026-09-24 16:48:15 +02:00
me0nline 4a352337a5 feat(server): add fleet licensing WebUI dashboard, .lic export, and automated mTLS renewals 2026-09-24 16:47:47 +02:00
me0nline 2fba971aeb feat(licensing): add Master License Authority program with CSR signing, exact seat quotas, and mTLS renewals 2026-09-24 16:42:10 +02:00
me0nline f605375639 feat(pki): add CA CSR generation, seat quota OID extensions, and mTLS renewal support 2026-09-24 16:40:54 +02:00
me0nline f5ff8ab6cc ci(windows): use native PowerShell git checkout to eliminate Node.js dependency on Windows runner
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m45s
Release Binaries & Installers / Build & Release Linux Binaries (push) Canceled after 0s
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Canceled after 0s
2026-09-04 23:54:13 +02:00
me0nline fd6da560ed docs: update release tag instructions for v2.0.1 in root README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m15s
2026-09-04 23:31:44 +02:00
me0nline 956dfc5d93 docs(release): publish release notes for LOGAR v2.0.1 2026-09-04 23:31:29 +02:00
me0nline 35e6a8df3e ci: bump default release tag to v2.0.1 in release.yml 2026-09-04 23:31:15 +02:00
me0nline c121291dda build(installer): bump server AppVersion to 2.0.1 in installer_server.iss 2026-09-04 23:31:03 +02:00
me0nline d19bbb2ec1 build(installer): bump client AppVersion to 2.0.1 in installer_client.iss 2026-09-04 23:30:50 +02:00
me0nline e6dc82ff55 chore(server): bump server version to 2.0.1 2026-09-04 23:30:36 +02:00
me0nline 56d8516427 docs: update README.md to reference consolidated release.yml workflow
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m24s
Release Binaries & Installers / Build & Release Linux Binaries (push) Canceled after 0s
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Canceled after 0s
2026-09-04 23:26:05 +02:00
me0nline 94ad3f9461 ci: remove obsolete release-windows.yml in favor of consolidated release.yml 2026-09-04 23:25:38 +02:00
me0nline 26a4509429 ci: remove obsolete release-linux.yml in favor of consolidated release.yml 2026-09-04 23:25:35 +02:00
me0nline 80ae42088f ci: add consolidated release.yml combining Linux and Windows release jobs 2026-09-04 23:25:32 +02:00
me0nline 42f01addca docs: document 25 unit tests and tripartite release options in root README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m21s
2026-09-04 23:23:08 +02:00
me0nline a9f096ef1b ci(windows): optimize tag triggers and add default v2.0.0 tag to release-windows.yml 2026-09-04 23:22:52 +02:00
me0nline 26d3d59812 ci(linux): optimize tag triggers and add default v2.0.0 tag to release-linux.yml 2026-09-04 23:22:38 +02:00
me0nline 5fec32327f ci: prevent ci.yml from triggering on tag pushes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m22s
2026-09-04 23:08:19 +02:00
me0nline 478807d873 ci(linux): add v* tag pattern to release-linux.yml 2026-09-04 23:08:06 +02:00
me0nline 149ba6dfec ci(windows): add release event and multi-pattern tag triggers to release-windows.yml 2026-09-04 23:07:54 +02:00
me0nline a11b05f0a9 docs: update release tag instructions for v2.0.0 in root README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m21s
2026-09-04 22:57:27 +02:00
me0nline 722d2a1fec docs(release): publish release notes for LOGAR v2.0.0 2026-09-04 22:57:17 +02:00
me0nline 5883b78822 build(installer): bump server AppVersion to 2.0.0 in installer_server.iss 2026-09-04 22:57:02 +02:00
me0nline d79de301bf build(installer): bump client AppVersion to 2.0.0 in installer_client.iss 2026-09-04 22:56:53 +02:00
me0nline 184fdc6bc6 docs: add certificate auto-renewal details to out/linux_client/README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Has been cancelled
2026-09-04 22:55:09 +02:00
me0nline 4625b650e5 docs: add certificate auto-renewal details to out/win_client/README.md 2026-09-04 22:54:59 +02:00
me0nline f38bbfb540 docs: add certificate watchdog details to out/linux_server/README.md 2026-09-04 22:54:47 +02:00
me0nline 18f0286692 docs: add certificate watchdog details to out/win_server/README.md 2026-09-04 22:54:36 +02:00
me0nline c9f769ef2b docs: document certificate validity watchdog and auto-renewal in root README.md 2026-09-04 22:54:25 +02:00
me0nline c01c09ecbd test(linux_client): add test_client_certificate_validity_and_proactive_check in test_linux_client.py 2026-09-04 22:52:49 +02:00
me0nline 2afe94fb36 test(win_client): add test_client_certificate_validity_and_proactive_check in test_win_client.py 2026-09-04 22:52:34 +02:00
me0nline 4650cbcafc test(server): add test_cert_validity_and_hub_pki_renewal in test_server.py 2026-09-04 22:52:18 +02:00
me0nline 0d613b3d22 feat(linux_client): add proactive certificate expiry check and reactive self-healing in Linux_Client.py 2026-09-04 22:52:00 +02:00
me0nline 916d3764a2 feat(win_client): add proactive certificate expiry check and reactive self-healing in Win_Client.py 2026-09-04 22:51:28 +02:00
me0nline 0e7d299594 feat(server): add in-flight certificate validity watchdog and dynamic SSLContext reloading in Server.py 2026-09-04 22:50:50 +02:00
me0nline 2cff629e23 feat(pki): add certificate expiration check and auto-renewal in server_enrollment.py 2026-09-04 22:49:54 +02:00
me0nline cfc633c398 docs(linux_server): update README.md with mTLS 1.3, dynamic PKI licensing, and systemd installer script instructions
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m3s
2026-09-04 22:39:48 +02:00
me0nline d61e343fbe docs(win_server): update README.md with mTLS 1.3, dynamic PKI licensing, and Inno Setup installer instructions 2026-09-04 22:39:25 +02:00
me0nline 4a446a2e73 docs(linux_client): update README.md with mTLS 1.3, dynamic PKI enrollment, and systemd installer script instructions 2026-09-04 22:39:09 +02:00
me0nline b24108a788 docs(win_client): update README.md with mTLS 1.3, dynamic PKI enrollment, and Inno Setup installer instructions 2026-09-04 22:37:20 +02:00
me0nline 1c985c85c8 docs: update README.md with mTLS architecture, dynamic PKI licensing, service installers, and updated test suite
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 5m9s
2026-09-04 22:29:30 +02:00
me0nline 249b754423 test(pipeline): update end-to-end integration test for mTLS and dynamic PKI enrollment in test_pipeline.py
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m17s
2026-09-04 20:55:12 +02:00
me0nline 16faad063d test(linux_client): add mTLS client certificate handling test in test_linux_client.py 2026-09-04 20:55:07 +02:00
me0nline c705be57eb test(win_client): add mTLS client certificate handling test in test_win_client.py 2026-09-04 20:55:01 +02:00
me0nline 3d6a2b86d6 test(server): add PKI, enrollment, and license quota tests in test_server.py 2026-09-04 20:54:54 +02:00
me0nline 1d0845b548 ci: include src/server_enrollment.py in syntax verification step 2026-09-04 20:54:49 +02:00
me0nline 11f16ed8e1 ci(windows): update release-windows.yml to trigger on v* tags and compile Inno Setup installers 2026-09-04 20:54:40 +02:00
me0nline 8cb3089e8e feat(installer): add Inno Setup script for LOGAR Server 2026-09-04 20:54:35 +02:00
me0nline 5bbb56b4c3 feat(installer): add Inno Setup script for LOGAR Client 2026-09-04 20:54:30 +02:00
me0nline a04d9bac9f chore(deps): bundle NSSM 64-bit binary for Windows service management 2026-09-04 20:54:24 +02:00
me0nline 8813d2d865 chore(git): unignore compilation/nssm.exe in .gitignore 2026-09-04 20:54:18 +02:00
me0nline 7e9f7a56f8 feat(install): add Linux server systemd installation script 2026-09-04 20:54:12 +02:00
me0nline 99920ef395 feat(install): add Linux client systemd installation script 2026-09-04 20:54:06 +02:00
me0nline ccb23d65e5 feat(linux_client): add mTLS connection and automatic enrollment in src/Linux_Client.py 2026-09-04 20:54:00 +02:00
me0nline fb9ef6e6cb feat(win_client): add mTLS connection and automatic enrollment in src/Win_Client.py 2026-09-04 20:53:54 +02:00
me0nline 491d2b1194 feat(server): add mTLS listener, client licensing schema, and enrollment endpoint in src/Server.py 2026-09-04 20:53:48 +02:00
me0nline e83a5b3e0f feat(pki): add dynamic PKI and client certificate generation in src/server_enrollment.py 2026-09-04 20:53:42 +02:00
me0nline 0901ccb3eb Merge branch 'origin/main' into main: adopt restructured repository layout 2026-09-04 20:29:29 +02:00
me0nline cb4c763e0e Add hierarchical wildcard to tags trigger in release-windows.yml
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m41s
Release Linux Binaries / Build & Release Linux Binaries (push) Failing after 28s
Release Windows Binaries / Build & Release Windows Binaries (push) Canceled after 0s
2026-09-04 17:54:57 +02:00
me0nline 84579d8719 Add hierarchical wildcard to tags trigger in release-linux.yml 2026-09-04 17:54:50 +02:00
me0nline 4e2242e0ae Name checksum files distinctively as SHA256SUMS-windows.txt and SHA256SUMS-linux.txt 2026-09-03 22:13:36 +02:00
51 changed files with 8396 additions and 1733 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"
+15 -13
View File
@@ -6,33 +6,35 @@ on:
- '**'
tags-ignore:
- '*'
- '**'
- 'v*'
pull_request:
workflow_dispatch:
jobs:
test:
name: Run Component Tests & Pipeline Verification
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/')"
runs-on: logar
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install System Dependencies & Python
- name: Install Dependencies
run: |
if command -v apt-get >/dev/null 2>&1; then
apt-get update -y
apt-get install -y python3 python3-pip python3-venv curl
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
python3 compilation/ensure_pip.py
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
python3 -m pip install -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install -r compilation/requirements.txt
- name: Verify Python Syntax
run: |
python3 -m py_compile src/Server.py src/Win_Client.py src/Linux_Client.py compilation/package_dist.py compilation/upload_release.py tests/test_pipeline.py tests/*.py
python3 -m py_compile src/Server.py src/server_enrollment.py src/Win_Client.py src/Linux_Client.py compilation/package_dist.py compilation/upload_release.py tests/test_pipeline.py tests/*.py
- name: Run Component Unit Tests
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
run: |
@@ -47,10 +49,10 @@ jobs:
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
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."
READY=1
break
-43
View File
@@ -1,43 +0,0 @@
name: Release Linux Binaries
on:
release:
types: [published, created]
push:
tags:
- '*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (optional)'
required: false
jobs:
release-linux:
name: Build & Release Linux Binaries
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install Python and Build Dependencies
run: |
if command -v apt-get >/dev/null 2>&1; then
apt-get update -y
apt-get install -y python3 python3-pip python3-venv binutils zip
fi
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
pip3 install pyinstaller -r compilation/requirements.txt --break-system-packages || pip3 install pyinstaller -r compilation/requirements.txt
- name: Compile Standalone Linux Binaries
run: |
python3 compilation/package_dist.py --target linux
- name: Publish Linux Release Assets
env:
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_REF_NAME: ${{ github.event.release.tag_name || inputs.tag || github.ref_name }}
run: |
python3 compilation/upload_release.py --skip-build
-66
View File
@@ -1,66 +0,0 @@
name: Release Windows Binaries
on:
release:
types: [published, created]
push:
tags:
- '*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (optional)'
required: false
jobs:
release-windows:
name: Build & Release Windows Binaries
runs-on: windows-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
continue-on-error: true
- 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 -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: 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: ${{ github.event.release.tag_name || inputs.tag || 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
+76
View File
@@ -0,0 +1,76 @@
name: Release Binaries & Packages
on:
push:
tags:
- 'v*'
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish assets to (default: v0.3.1)'
required: false
default: 'v0.3.1'
jobs:
release:
name: Build & Release Standalone Binaries (Linux & Windows Cross-Compilation)
runs-on: logar
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install System and Cross-Compilation Build Dependencies
run: |
echo "=== SYSTEM DIAGNOSTICS ==="
uname -a || true
python3 --version || true
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
if command -v apt-get >/dev/null 2>&1; then
echo "[*] Installing Python shared libraries, headers, NSIS via apt-get..."
export DEBIAN_FRONTEND=noninteractive
$SUDO apt-get clean || true
$SUDO apt-get update --allow-releaseinfo-change -o Acquire::Check-Valid-Until=false -y || $SUDO apt-get update -y || true
$SUDO apt-get -f install -y || true
PY_VER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "3.9")
echo "[*] Detected Python version: ${PY_VER}"
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends \
nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" \
python3-dev libpython3-dev build-essential gcc make curl tar gzip zip unzip || true
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends nsis nsis-common || true
for pkg in nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" python3-dev libpython3-dev build-essential; do
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends "$pkg" || true
done
echo "[*] Verifying makensis:"
which makensis || echo "makensis not in PATH"
echo "[*] Verifying libpython shared library:"
find /usr/lib /usr/local/lib -name "libpython*.so*" 2>/dev/null || true
elif command -v apk >/dev/null 2>&1; then
echo "[*] Installing dependencies via apk..."
$SUDO apk add --no-cache nsis python3-dev build-base gcc make curl tar gzip zip unzip || true
elif command -v dnf >/dev/null 2>&1; then
echo "[*] Installing dependencies via dnf..."
$SUDO dnf install -y mingw32-nsis nsis python3-devel gcc make curl tar gzip zip unzip || true
fi
python3 compilation/ensure_pip.py
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt
- name: Compile Standalone Binaries (Linux & Windows)
run: |
python3 compilation/package_dist.py --target all
- name: Publish Unified Release Assets
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: |
python3 compilation/upload_release.py --skip-build
+4
View File
@@ -11,6 +11,7 @@ build/
dist/
*.spec
*.exe
!compilation/nssm.exe
*.bin
*.dll
*.so
@@ -28,6 +29,9 @@ server_config.json
client_config.json
test_*.json
test_*.db
.env
*.env
runner/.env
# IDE & OS files
.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 e6d7fa5b54
+120 -337
View File
@@ -1,387 +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 OpenPGP encryption, authenticated TCP streaming, 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.2-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
1. [Core Philosophy](#core-philosophy)
2. [Architecture & Data Flow](#architecture--data-flow)
3. [Security & Cryptographic Model](#security--cryptographic-model)
4. [Cloud-Side Temporal Persistence & 4-Run Rule](#cloud-side-temporal-persistence--4-run-rule)
5. [Agentic Hermes Integration](#agentic-hermes-integration)
6. [Dynamic Machine & Domain Identification](#dynamic-machine--domain-identification)
7. [Repository & Shippables Structure](#repository--shippables-structure)
8. [Getting Started & Installation](#getting-started--installation)
9. [Running Tests](#running-tests)
10. [Automated Releases via Gitea Actions](#automated-releases-via-gitea-actions)
## Key Capabilities
- **Zero-State Edge Forwarders**: Windows (`Win_Client`) and Linux (`Linux_Client`) agents maintain zero local databases, zero cache files, and minimal memory footprint.
- **Mutual TLS 1.3 (`:9443`)**: Enforces bidirectional cryptographic identity verification (`ssl.CERT_REQUIRED`) for all streaming log frames.
- **Hierarchical PKI & Seat Licensing**: Subordinate CAs automatically enroll and sync seat quotas with the Master Authority (`logar.eibl.tech`) over WebSockets.
- **12-Hour Temporal Persistence**: Evaluates anomalies over a rolling 12-hour temporal window, tracking occurrence velocity and first/last seen metrics.
- **Direct Hermes AI Provisioning (`:8443`)**: Exposes verified anomalous telemetry directly to Hermes AI via `/api/hermes/report` without heuristic bottlenecks.
- **WebUI Dashboard**: Authenticated, 2-tab management console for live anomaly telemetry, client seat leasing, and Sub-CA lifecycle management.
---
## Core Philosophy
### 1. Edge Thinness & Zero State
Site agents running on Windows and Linux act strictly as lightweight forwarders:
- **No Local Database**: Clients maintain zero state and no local SQLite or cache files.
- **Source-Level Filtering**: Agents upload candidate entries spanning from informational events up to errors (`INFO`, `WARNING`, `ERROR`, `CRITICAL`), while stripping verbose debugging noise (`DEBUG`, audit entries) and skipping any entries older than 24 hours.
- **End-to-End Encryption**: Logs are encrypted using the server's OpenPGP public key before leaving the edge node.
- **Secure TCP Sockets**: Ingestion occurs over low-overhead authenticated TCP sockets rather than bulky HTTP/HTTPS endpoints.
### 2. Cloud-Side Temporal Persistence & Severity Routing
The central Python/TCP hub handles the heavy lifting:
- 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` level issues must persist across **at least 4 consecutive runs / cycles** within the 12-hour window to be confirmed as genuine anomalies, automatically filtering out transient 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 human engineers manually diving through noisy 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 team notifications.
---
## Architecture & Data Flow
## System Architecture
```mermaid
graph TB
subgraph Edge Nodes [State-Tracking Edge Forwarders]
W[Win_Client.py / Win_Client.exe / Win_Client.pyz<br/>Windows Event Log Application]
L[Linux_Client.py / Linux_Client.bin<br/>systemd journalctl -p info]
subgraph Authority [Master License Authority - logar.eibl.tech]
M_CA[Master Root CA]
AUTH_WS[WebSocket Licensing Hub]
end
subgraph Security Layer [Security & Framing]
E[OpenPGP Payload Encryption<br/>Server Public Key & Fingerprint]
S[Length-Prefixed Framing<br/>4-byte Big-Endian + Auth Envelope]
subgraph ServerHub [LOGAR Server Hub]
SUB_CA[Subordinate CA]
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
subgraph Cloud Hub [LOGAR Central Server Hub]
TCP[Authenticated TCP Listener<br/>Port 9443]
DEC[OpenPGP Decryption<br/>Server Private Key]
DB[(SQLite Persistence<br/>active_issues & ingest_runs)]
RULE{12h Window &<br/>4-Run Rule}
subgraph EdgeNodes [Heterogeneous Edge Forwarders]
WIN[Win_Client.exe<br/>Windows Event Logs]
LIN[Linux_Client.bin<br/>systemd journalctl]
end
subgraph Agentic Reporting [Downstream Integration]
API[FastAPI / Uvicorn Reporting<br/>Port 8443]
HERMES[Hermes Agent<br/>GET /api/hermes/report]
subgraph Downstream [Hermes AI Layer]
HERMES[Hermes AI Engine]
end
W --> E
L --> E
E --> S
S -->|TCP Stream| TCP
TCP --> DEC
DEC --> RULE
RULE --> DB
DB --> API
API --> HERMES
M_CA -->|Signs Sub-CA Cert| SUB_CA
AUTH_WS <==>|Live WebSocket Sync| SUB_CA
WIN -->|1. Auto-Enrollment| SUB_CA
LIN -->|1. Auto-Enrollment| SUB_CA
WIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
LIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
PROV_API ==>|3. GET /api/hermes/report| HERMES
```
---
## Security & Cryptographic Model
## Quick Start & Automated Installers
### Pure-Python OpenPGP (RFC 4880)
- **Zero OS Binary Dependency**: Utilizes `pgpy` and `cryptography` in pure Python. **No native GnuPG or `gpg` binary installation is required** on the server, Windows nodes, or Linux nodes.
- **First-Run Automatic Key Generation**: On the first launch, if `server_config.json` is missing, `Server.py` automatically generates:
- An OpenPGP RSA 2048 keypair with encryption-only usage flags.
- An armored private key (`private_key`) and public key (`public_key`).
- A SHA-256 public encryption fingerprint (`server_fingerprint`).
- A cryptographically random authentication secret token (`auth_token`).
- **Client Configuration Exporter**:
LOGAR provides automated production service installers for both Linux (`systemd`) and Windows (`services.msc` via NSSM):
### 1. Server Hub Installation
- **Linux (Automated systemd Installer)**:
```bash
python src/Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
sudo ./compilation/install_linux_server.sh
```
Produces an anonymous client config containing only the server socket coordinates, authentication token, and the encryption-only public key & fingerprint.
- **Socket Protocol Framing**:
- `[4 bytes big-endian unsigned int]` : Total envelope length.
- `[JSON Envelope]` :
```json
{
"auth_token": "<SECRET_TOKEN>",
"timestamp": "2026-09-03T...",
"encrypted_payload": "-----BEGIN PGP MESSAGE-----\n..."
}
```
- Unauthorized clients or invalid authentication tokens are rejected immediately.
*Deploys standalone `Server.bin` to `/opt/logar-server`, initializes PKI, and enables `logar-server.service`.*
- **Windows (Inno Setup / NSIS Installer)**:
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.
#### 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).*
---
## 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**:
- When an issue is observed, the hub compares `(now - last_seen)`.
- If more than 12 hours have passed since the issue was last recorded, the previous window is expired and the cycle resets to `run_count = 1`.
- **4-Run Rule for Warnings**:
- The 4-run persistence threshold specifically applies to `WARNING` (and `INFO`) events to eliminate transient operational noise.
- For each distinct run batch, `run_count` increments.
- 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 Integration
The server hub serves a REST reporting API (default port `8443`):
### `GET /api/hermes/report`
Returns all **verified anomalies** (immediate critical errors and warnings verified after 4 consecutive runs within the 12-hour evaluation window):
```json
[
{
"fingerprint": "corp.internal:web-app-01.corp.internal:NginxWorkerCrash",
"site": "corp.internal",
"server": "web-app-01.corp.internal",
"signature": "NginxWorkerCrash",
"severity": "ERROR",
"message": "Worker process 4120 terminated with signal 11",
"os_type": "linux",
"first_seen": "2026-09-03T09:00:00+00:00",
"last_seen": "2026-09-03T21:00:00+00:00",
"consecutive_runs": 4,
"evaluation_window": "12h",
"verified": true,
"status": "VERIFIED"
}
]
```
### `GET /api/hermes/all`
Diagnostic endpoint listing all active issues (both `TRANSIENT` candidate blips and `VERIFIED` anomalies).
### `GET /health`
Returns hub health, encryption fingerprint, and listener ports.
---
## Dynamic Machine & Domain Identification
Client configurations intentionally contain **no 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 variable `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`).
---
## Repository & Shippables Structure
## Repository Structure
```
LOGAR/
├── .gitea/
│ └── workflows/
│ ├── ci.yml # Continuous Integration automated test suite (runs on every push)
│ ├── release-linux.yml # Linux release workflow (compiles Server.bin, Linux_Client.bin, checksums)
│ └── release-windows.yml # Windows release workflow (compiles Server.exe, Win_Client.exe, checksums)
├── compilation/ # Build, packaging, and release automation tools
│ ├── package_dist.py # Multi-platform standalone binary packaging script
│ ├── requirements.txt # Unified project dependencies
│ └── upload_release.py # Direct Gitea REST API release asset publisher
├── src/ # Core application source modules
│ ├── __init__.py
│ ├── Server.py # Central TCP server and Hermes API
│ ├── Win_Client.py # Windows edge forwarder
│ └── Linux_Client.py # Linux edge forwarder
├── tests/ # Automated test suites
│ ├── test_linux_client.py # Linux client unit tests
│ ├── test_pipeline.py # End-to-end integration test
│ ├── test_server.py # Server unit tests
│ └── test_win_client.py # Windows client unit tests
├── .gitignore # Ignore venv, caches, DBs, and private keys
├── README.md # Comprehensive documentation
├── RELEASE_NOTES.md # Release history and changelog
├── server_config.sample.json # Central server sample configuration
└── out/ # Standalone deployment documentation & sample configs
├── linux_server/
│ ├── README.md # Linux systemd service installation & hub guide
│ └── server_config.sample.json # Reference server configuration
├── win_server/
│ ├── README.md # Windows service (NSSM/Task Scheduler) installation guide
│ └── server_config.sample.json # Reference server configuration
├── linux_client/
│ ├── README.md # Linux service & timer installation guide
│ └── client_config.sample.json # Reference client configuration
└── win_client/
├── README.md # Windows service installation & configuration guide
└── client_config.sample.json # Reference client configuration
├── .gitea/workflows/ # CI/CD pipelines (ci.yml, release.yml)
├── compilation/ # Build scripts, Linux installers & Inno Setup scripts
│ ├── install_linux_client.sh # Linux client systemd auto-installer
│ ├── install_linux_server.sh # Linux server systemd auto-installer
│ ├── package_dist.py # Standalone binary cross-compiler
│ └── upload_release.py # Gitea release asset publisher
├── src/ # Application source code
│ ├── Server.py # Central mTLS hub, temporal engine & REST API
│ ├── server_enrollment.py # Sub-CA PKI and certificate generator
│ ├── Win_Client.py # Windows edge forwarder
│ └── Linux_Client.py # Linux edge forwarder
├── tests/ # Automated test suites (35 unit tests + pipeline test)
├── LOGAR-LICENSING/ # Standalone Master License Authority (Git submodule)
├── RELEASE_NOTES.md # Version history and security changelog
└── README.md # Repository overview
```
---
## Getting Started & Installation
## 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**:
```bash
pip install -r compilation/requirements.txt
```
2. **Start the server** (generates `server_config.json` and keypair 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
```
### Core Endpoints
### 2. Windows Client Deployment
#### Option A: Precompiled Standalone Executable (Recommended)
1. Download `Win_Client.exe` (or `Win_Client.pyz`) from the repository releases.
2. Place `client_config.json` (exported from the server) in the same directory.
3. Run manually or schedule via Task Scheduler (every 3 hours):
```powershell
.\Win_Client.exe --hours 24
```
#### Option B: Python Source Execution
1. Copy `Win_Client.py`, `requirements.txt`, and `client_config.json` to the target machine.
2. Install client dependencies:
```powershell
python -m pip install -r requirements.txt
```
3. Run manually or schedule via Task Scheduler:
```powershell
python Win_Client.py --hours 24
```
### 3. Linux Client Deployment
#### Option A: Precompiled Standalone Binary (Recommended)
1. Download `Linux_Client.bin` from the repository releases.
2. Place `Linux_Client.bin` and `client_config.json` into `/opt/logar/` and make it executable:
```bash
chmod +x /opt/logar/Linux_Client.bin
```
3. Run via cron or systemd timer:
```bash
0 */3 * * * /opt/logar/Linux_Client.bin --hours 24
```
#### Option B: Python Source Execution
1. Copy `Linux_Client.py`, `requirements.txt`, and `client_config.json` to `/opt/logar/`.
2. Install client dependencies:
```bash
python3 -m pip install -r requirements.txt
```
3. (Optional) Run `out/linux_client/build_bin.sh` to compile a standalone ELF binary locally if desired.
4. Run via cron or systemd timer:
```bash
0 */3 * * * python3 /opt/logar/Linux_Client.py --hours 24
```
- **`POST /api/client/enroll`**: Forwarder auto-enrollment and proactive certificate renewal.
- **`GET /api/clients`**: Returns active seat quota and registered edge forwarder telemetry *(Admin Auth)*.
- **`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)*.
- **`GET /health`**: Server health, encryption fingerprint, and mTLS status.
---
## Running Tests
### 1. Component-Specific Unit Tests
The test suite is located in `tests/` and exercises all components:
## Testing & Cross-Compilation
```bash
# Run all unit tests
python -m unittest discover -s tests
# Run full unit test suite (35 tests)
python -m unittest discover -s tests -v
# Or run component tests individually:
python -m unittest tests/test_server.py
python -m unittest tests/test_win_client.py
python -m unittest tests/test_linux_client.py
# Run end-to-end mTLS pipeline integration test
python tests/test_pipeline.py
# Cross-compile native Linux and Windows binaries on Linux runner
python compilation/package_dist.py --target all
```
### 2. End-to-End Pipeline Integration Test
The pipeline test exercises invalid token rejection, encrypted socket streaming, database persistence, status promotion upon the 4th run, and the Hermes API report output.
1. **Start the server** in Shell 1 (creates `server_config.json` on first run):
```bash
python src/Server.py
```
2. **Export client configuration** in Shell 2 (required for testing):
```bash
python src/Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
```
3. **Execute the integration test** in Shell 2:
```bash
python tests/test_pipeline.py
```
## Continuous Integration via Gitea Actions
Continuous integration is automated via [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) and triggers automatically on **every push** and pull request:
1. **Syntax Compilation**: Validates all Python scripts (`src/Server.py`, `src/Win_Client.py`, `src/Linux_Client.py`, `compilation/package_dist.py`, `compilation/upload_release.py`, `tests/test_pipeline.py`, and test suites).
2. **Component Unit Tests**: Discovers and runs all unit tests in `tests/` (`test_server.py`, `test_win_client.py`, `test_linux_client.py`).
3. **End-to-End Pipeline Verification**: Automatically spins up the LOGAR server hub, generates test configs, runs `tests/test_pipeline.py` (testing socket authentication, 4-run rule persistence, Hermes API report, and client integrations), and shuts down the test instance.
---
## Automated Releases via Gitea Actions
## Documentation & Deep Dives
Release builds are automated via two dedicated Gitea Actions workflows running concurrently on native platform runners:
- [`.gitea/workflows/release-linux.yml`](.gitea/workflows/release-linux.yml) (`ubuntu-latest`)
- [`.gitea/workflows/release-windows.yml`](.gitea/workflows/release-windows.yml) (`windows-latest`)
For in-depth operational guides, lifecycle state machines, and PKI specifications, consult the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**:
### Publishing a Release
Whenever you want to release a new version with compiled standalone binaries:
```bash
git tag v1.0.2
git push origin v1.0.2
```
*(You can also trigger builds manually via the Gitea UI using the **Run workflow** button (`workflow_dispatch`) on either workflow).*
### Automated Multi-Platform Compilation:
1. **Linux Runner** (`release-linux.yml`):
- Compiles native Linux ELF executables: `Linux_Client.bin` and `Server.bin`.
- Generates dedicated SHA-256 checksum files:
- `linux_client_sha256sum` (verification for `Linux_Client.bin`)
- `linux_agent_sha256sum` (alias for client/agent integrations)
- `linux_server_sha256sum` (verification for `Server.bin`)
- `SHA256SUMS_linux.txt` (summary manifest)
- Attaches all Linux assets to the Gitea release.
2. **Windows Runner** (`release-windows.yml`):
- Compiles native Windows PE executables: `Win_Client.exe` and `Server.exe`.
- Generates dedicated SHA-256 checksum files:
- `win_client_sha256sum` (verification for `Win_Client.exe`)
- `win_agent_sha256sum` (alias for client/agent integrations)
- `win_server_sha256sum` (verification for `Server.exe`)
- `SHA256SUMS_windows.txt` (summary manifest)
- Attaches all Windows assets to the Gitea release.
3. **Concurrent Publishing & Conflict Handling**:
`upload_release.py` includes automatic retry and conflict resolution so concurrent Windows and Linux runners attach their respective assets to the release without collision.
### Verifying Checksums
- On Linux:
```bash
sha256sum -c linux_client_sha256sum
# or
sha256sum -c linux_server_sha256sum
```
- On Windows (PowerShell):
```powershell
Get-FileHash .\Win_Client.exe -Algorithm SHA256
Get-Content .\win_client_sha256sum
```
### Local Packaging & Manual Upload
You can also compile and package binaries locally anytime:
```bash
# Windows
py -3.12 compilation/package_dist.py --target windows
# Linux
python3 compilation/package_dist.py --target linux
```
To upload local builds directly to Gitea:
```powershell
python compilation/upload_release.py --tag v1.0.2 --token <YOUR_GITEA_TOKEN>
```
*(Environment variables `GITEA_TOKEN`, `GITEA_SERVER_URL`, `GITEA_REPOSITORY`, and `GITEA_REF_NAME` are also supported automatically).*
* [Architecture & Design Details](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Architecture-and-Design)
* [System State Machines & Subsystem Lifecycles](https://gitea.eibl.tech/me0nline/LOGAR/wiki/State-Machines-and-Lifecycles)
* [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
+5 -12
View File
@@ -1,13 +1,6 @@
# LOGAR Release v1.0.2
# LOGAR Release v0.3.2
### Changes in this Release:
- **Warning Persistence & Immediate Error Routing**: Restructured temporal verification on the central hub so the 4-run persistence rule across the 12-hour evaluation window strictly governs `WARNING` and `INFO` events to suppress transient blips. High-severity `ERROR`, `CRITICAL`, and `FATAL` events are now promoted to `VERIFIED` immediately on their first occurrence and reported to Hermes without waiting for consecutive runs.
- **Server Deployment Packages in `out/`**: Added comprehensive deployment guides and configuration templates for both Linux Server hub (systemd service) and Windows Server hub (NSSM service / Task Scheduler) under `out/linux_server` and `out/win_server`.
- **Refactored Repository Layout**: Reorganized codebase by moving runtime forwarders and server hub into `src/`, compilation/release packaging utilities into `compilation/`, and all unit and pipeline verification tests into `tests/`.
- **Dual Platform Gitea Release Automation**: Dedicated Windows (`release-windows.yml`) and Linux (`release-linux.yml`) Gitea Actions to compile native platform binaries (`Win_Client.exe` and `Server.exe` on Windows; `Linux_Client.bin` and `Server.bin` on Linux).
- **Dedicated SHA-256 Checksums**: Release assets now include dedicated checksum files matching `[win/linux]_[client/agent]_sha256sum` (`win_client_sha256sum`, `win_agent_sha256sum`, `win_server_sha256sum`, `linux_client_sha256sum`, `linux_agent_sha256sum`, `linux_server_sha256sum`).
- **Removed Client Filter Logic**: Removed restrictive source-level noise filtering on edge forwarders. Clients now collect and stream all candidate events from `INFO` up to `ERROR` over the lookback window instead of discarding them at the source.
- **State Tracking & Deduplication**: Added persistent client state tracking (`client_state.json`) with journalctl cursors and Windows Event Log record numbers to guarantee that previously transmitted events are never resent.
- **24-Hour Lookback Window**: Forwarders now scan and upload events from the last 24 hours (default `--hours 24`), skipping older entries.
- **Lightweight Distribution Structure**: Cleaned `out/` to strictly contain deployment documentation and sample configurations.
- **Automated Gitea CI/CD**: Integrated push testing workflow (`ci.yml`) and multi-platform release asset packaging.
### 🪟 Windows Installer Fixes
- **Standalone Windows Binaries & Python Runtime**: Resolved issues with standalone Windows executable execution and bundled runtime paths.
- **Data Directory Selection & Permissions**: Added custom data directory selection during setup and fixed permissions for background Windows services.
- **Service Lifecycle & Clean Uninstallation**: Resolved service startup issues and ensured complete cleanup of state files upon uninstallation.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0E1A2E"/>
<stop offset="50%" stop-color="#08101E"/>
<stop offset="100%" stop-color="#040810"/>
</linearGradient>
<linearGradient id="cyanGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00E5FF"/>
<stop offset="100%" stop-color="#0072FF"/>
</linearGradient>
<linearGradient id="emeraldGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#00FFA3"/>
<stop offset="100%" stop-color="#00E5FF"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="12" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Base Hexagon Shield -->
<polygon points="256,24 456,139 456,373 256,488 56,373 56,139"
fill="url(#bgGrad)" stroke="url(#cyanGrad)" stroke-width="8" filter="url(#glow)"/>
<!-- Inner Telemetry Grid / Ring -->
<polygon points="256,58 426,156 426,356 256,454 86,356 86,156"
fill="none" stroke="#162A45" stroke-width="3" stroke-dasharray="8 6"/>
<!-- Stylized L and Log Streams -->
<rect x="140" y="140" width="46" height="232" rx="10" fill="url(#cyanGrad)" filter="url(#glow)"/>
<rect x="140" y="326" width="220" height="46" rx="10" fill="url(#cyanGrad)" filter="url(#glow)"/>
<rect x="220" y="210" width="36" height="80" rx="8" fill="url(#emeraldGrad)"/>
<rect x="286" y="150" width="36" height="140" rx="8" fill="url(#cyanGrad)"/>
<!-- Connection Nodes / Spark Points -->
<circle cx="238" cy="190" r="7" fill="#00FFA3" filter="url(#glow)"/>
<circle cx="304" cy="130" r="7" fill="#00E5FF" filter="url(#glow)"/>
<circle cx="380" cy="349" r="8" fill="#00E5FF" filter="url(#glow)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

+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()
+365
View File
@@ -0,0 +1,365 @@
"""
LOGAR Branding & Installer Graphics Generator
Generates clean, professional SVG, PNG, ICO, and BMP visual assets for
LOGAR installers (Inno Setup & NSIS), application icons, and WebUI branding.
Zero external dependencies required (uses standard library zlib and struct).
"""
import os
import math
import zlib
import struct
ASSETS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
os.makedirs(ASSETS_DIR, exist_ok=True)
def create_bmp_24bit(width: int, height: int, pixel_func) -> bytes:
"""Generates an uncompressed 24-bit RGB BMP image."""
row_bytes = width * 3
padding = (4 - (row_bytes % 4)) % 4
image_size = (row_bytes + padding) * height
file_size = 14 + 40 + image_size
# BITMAPFILEHEADER (14 bytes)
header = struct.pack(
"<2sIHHI",
b"BM",
file_size,
0, 0,
54 # Offset to pixel data
)
# BITMAPINFOHEADER (40 bytes)
info = struct.pack(
"<IIIHHIIIIII",
40, # biSize
width, # biWidth
height, # biHeight (positive = bottom-to-top)
1, # biPlanes
24, # biBitCount (24-bit BGR)
0, # biCompression (BI_RGB)
image_size, # biSizeImage
2835, # biXPelsPerMeter (72 DPI)
2835, # biYPelsPerMeter
0, # biClrUsed
0 # biClrImportant
)
data = bytearray()
pad = b"\x00" * padding
# BMP is stored bottom-to-top
for y in range(height - 1, -1, -1):
for x in range(width):
r, g, b = pixel_func(x, y, width, height)
data.extend(bytes([int(b) & 0xFF, int(g) & 0xFF, int(r) & 0xFF]))
data.extend(pad)
return header + info + bytes(data)
def create_png_rgba(width: int, height: int, pixel_func) -> bytes:
"""Generates a standard 32-bit RGBA PNG image."""
raw_scanlines = bytearray()
for y in range(height):
raw_scanlines.append(0) # Filter type 0 (None)
for x in range(width):
r, g, b, a = pixel_func(x, y, width, height)
raw_scanlines.extend(bytes([int(r) & 0xFF, int(g) & 0xFF, int(b) & 0xFF, int(a) & 0xFF]))
compressed = zlib.compress(bytes(raw_scanlines), level=9)
def chunk(tag: bytes, data: bytes) -> bytes:
chk = tag + data
crc = zlib.crc32(chk) & 0xFFFFFFFF
return struct.pack(">I", len(data)) + chk + struct.pack(">I", crc)
ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
png_bytes = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", compressed) + chunk(b"IEND", b"")
return png_bytes
def draw_logo_rgba(x: int, y: int, w: int, h: int) -> tuple:
"""Renders a sleek modern cyberpunk hexagon shield and pulse log streams."""
nx = (x - w / 2) / (w / 2)
ny = (y - h / 2) / (h / 2)
dist = math.sqrt(nx * nx + ny * ny)
if dist > 0.98:
return 0, 0, 0, 0
# Hexagon radius formula
angle = math.atan2(ny, nx)
hex_radius = 0.88 / (math.cos((angle % (math.pi / 3)) - (math.pi / 6)) + 0.001)
if dist > hex_radius:
if dist < hex_radius + 0.04:
alpha = max(0.0, min(1.0, (hex_radius + 0.04 - dist) / 0.04))
return 11, 25, 44, int(alpha * 255)
return 0, 0, 0, 0
# Gradient background: Dark Slate Blue to Navy
grad_t = (ny + 1) / 2
bg_r = int(12 * (1 - grad_t) + 6 * grad_t)
bg_g = int(24 * (1 - grad_t) + 14 * grad_t)
bg_b = int(48 * (1 - grad_t) + 32 * grad_t)
# Hexagon outer border (Cyber Cyan / Emerald highlight)
if dist > hex_radius - 0.07:
glow_t = (nx + 1) / 2
br = int(0 * (1 - glow_t) + 0 * glow_t)
bg = int(229 * (1 - glow_t) + 160 * glow_t)
bb = int(255 * (1 - glow_t) + 255 * glow_t)
return br, bg, bb, 255
# Center stylized 'L' log symbol and dynamic telemetry bars
# Bar 1 (Left Log Stream): x in [-0.42, -0.22], y in [-0.45, 0.45]
if -0.42 <= nx <= -0.22 and -0.45 <= ny <= 0.45:
v = (ny + 0.45) / 0.9
return int(0), int(220 * (1 - v) + 140 * v), int(255), 255
# Bar 2 (Bottom Base of 'L'): x in [-0.42, 0.35], y in [0.25, 0.45]
if -0.42 <= nx <= 0.35 and 0.25 <= ny <= 0.45:
v = (nx + 0.42) / 0.77
return int(0), int(220 * (1 - v) + 180 * v), int(255), 255
# Bar 3 (Central Stream Wave 1): x in [-0.10, 0.08], y in [-0.30, 0.12]
if -0.10 <= nx <= 0.08 and -0.30 <= ny <= 0.12:
return 0, 240, 255, 255
# Bar 4 (Central Stream Wave 2): x in [0.18, 0.36], y in [-0.50, 0.12]
if 0.18 <= nx <= 0.36 and -0.50 <= ny <= 0.12:
return 0, 180, 255, 255
# Subtle inner grid dots
grid_x = (nx * 10) % 1
grid_y = (ny * 10) % 1
if 0.45 <= grid_x <= 0.55 and 0.45 <= grid_y <= 0.55:
return bg_r + 20, bg_g + 30, bg_b + 50, 255
return bg_r, bg_g, bg_b, 255
def draw_wizard_header_white_bgr(x: int, y: int, w: int, h: int) -> tuple:
"""
Renders header BMP graphic with a pure white background (255, 255, 255)
matching the installer wizard dialog background seamlessly.
The logo is positioned on the left with clean alpha-blended edges.
"""
bg_r, bg_g, bg_b = 255, 255, 255
# Position emblem cleanly on the left (center x = 30, center y = h / 2)
icon_center_x = 28
icon_center_y = h / 2
icon_radius = 22
dx = (x - icon_center_x) / icon_radius
dy = (y - icon_center_y) / icon_radius
d = math.sqrt(dx * dx + dy * dy)
if d < 1.0:
er, eg, eb, ea = draw_logo_rgba(int((dx + 1) * 32), int((dy + 1) * 32), 64, 64)
alpha = ea / 255.0
r = int(er * alpha + bg_r * (1.0 - alpha))
g = int(eg * alpha + bg_g * (1.0 - alpha))
b = int(eb * alpha + bg_b * (1.0 - alpha))
return r, g, b
return bg_r, bg_g, bg_b
def draw_wizard_small_bgr(x: int, y: int, w: int, h: int) -> tuple:
"""Renders 55x55 square header BMP with centered logo on pure white background."""
bg_r, bg_g, bg_b = 255, 255, 255
cx = w / 2
cy = h / 2
rad = (min(w, h) / 2) - 4
dx = (x - cx) / rad
dy = (y - cy) / rad
d = math.sqrt(dx * dx + dy * dy)
if d < 1.0:
er, eg, eb, ea = draw_logo_rgba(int((dx + 1) * 32), int((dy + 1) * 32), 64, 64)
alpha = ea / 255.0
r = int(er * alpha + bg_r * (1.0 - alpha))
g = int(eg * alpha + bg_g * (1.0 - alpha))
b = int(eb * alpha + bg_b * (1.0 - alpha))
return r, g, b
return bg_r, bg_g, bg_b
def draw_wizard_sidebar_bgr(x: int, y: int, w: int, h: int) -> tuple:
"""Renders 164x314 BMP sidebar graphic for NSIS / Inno Setup."""
t_y = y / h
# Deep cyber navy to midnight blue vertical gradient
r = int(10 * (1 - t_y) + 4 * t_y)
g = int(22 * (1 - t_y) + 12 * t_y)
b = int(48 * (1 - t_y) + 26 * t_y)
# Subtle telemetry grid / stream lines
if int(x) % 18 == 0 or int(y) % 24 == 0:
r = min(255, r + 8)
g = min(255, g + 14)
b = min(255, b + 25)
# Decorative node glow in top region
emblem_cx = w / 2
emblem_cy = 90
edx = (x - emblem_cx) / 44
edy = (y - emblem_cy) / 44
ed = math.sqrt(edx * edx + edy * edy)
if ed < 0.98:
er, eg, eb, ea = draw_logo_rgba(int((edx + 1) * 64), int((edy + 1) * 64), 128, 128)
if ea > 128:
return er, eg, eb
# Glowing pulse lines streaming downwards from the emblem
if 55 <= x <= 110 and y > 150:
line_idx = int((x - 55) / 14)
line_offset = (line_idx * 37 + int(y * 0.4)) % 100
if line_offset < 30:
glow = math.sin((line_offset / 30) * math.pi)
g = min(255, int(g + 160 * glow))
b = min(255, int(b + 220 * glow))
# Bottom accent band
if y >= h - 4:
accent_t = x / w
return 0, int(220 * accent_t), 255
return r, g, b
def generate_ico_file(png_images: list) -> bytes:
"""Packs multiple PNG images into a valid Windows .ico container."""
count = len(png_images)
header = struct.pack("<HHH", 0, 1, count) # Reserved=0, Type=1 (ICO), Count
entries = bytearray()
offset = 6 + (16 * count)
image_data_blocks = []
for w, h, png_bytes in png_images:
size = len(png_bytes)
b_w = w if w < 256 else 0
b_h = h if h < 256 else 0
entry = struct.pack(
"<BBBBHHII",
b_w,
b_h,
0, # Color count
0, # Reserved
1, # Color planes
32, # Bits per pixel
size,
offset
)
entries.extend(entry)
image_data_blocks.append(png_bytes)
offset += size
return header + bytes(entries) + b"".join(image_data_blocks)
def generate_svg_logo() -> str:
"""Generates a high-quality SVG vector graphic for LOGAR."""
return """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0E1A2E"/>
<stop offset="50%" stop-color="#08101E"/>
<stop offset="100%" stop-color="#040810"/>
</linearGradient>
<linearGradient id="cyanGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00E5FF"/>
<stop offset="100%" stop-color="#0072FF"/>
</linearGradient>
<linearGradient id="emeraldGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#00FFA3"/>
<stop offset="100%" stop-color="#00E5FF"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="12" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Base Hexagon Shield -->
<polygon points="256,24 456,139 456,373 256,488 56,373 56,139"
fill="url(#bgGrad)" stroke="url(#cyanGrad)" stroke-width="8" filter="url(#glow)"/>
<!-- Inner Telemetry Grid / Ring -->
<polygon points="256,58 426,156 426,356 256,454 86,356 86,156"
fill="none" stroke="#162A45" stroke-width="3" stroke-dasharray="8 6"/>
<!-- Stylized L and Log Streams -->
<rect x="140" y="140" width="46" height="232" rx="10" fill="url(#cyanGrad)" filter="url(#glow)"/>
<rect x="140" y="326" width="220" height="46" rx="10" fill="url(#cyanGrad)" filter="url(#glow)"/>
<rect x="220" y="210" width="36" height="80" rx="8" fill="url(#emeraldGrad)"/>
<rect x="286" y="150" width="36" height="140" rx="8" fill="url(#cyanGrad)"/>
<!-- Connection Nodes / Spark Points -->
<circle cx="238" cy="190" r="7" fill="#00FFA3" filter="url(#glow)"/>
<circle cx="304" cy="130" r="7" fill="#00E5FF" filter="url(#glow)"/>
<circle cx="380" cy="349" r="8" fill="#00E5FF" filter="url(#glow)"/>
</svg>"""
def main():
print("[*] Generating high-resolution branding assets for LOGAR...")
# 1. Generate SVG Logo
svg_path = os.path.join(ASSETS_DIR, "logo.svg")
with open(svg_path, "w", encoding="utf-8") as f:
f.write(generate_svg_logo())
print(f"[+] Saved SVG logo: {svg_path}")
# 2. Generate PNG images of various sizes
png_sizes = [16, 32, 48, 64, 128, 256, 512]
png_buffers = []
for s in png_sizes:
png_data = create_png_rgba(s, s, draw_logo_rgba)
png_buffers.append((s, s, png_data))
if s in [256, 512]:
out_p = os.path.join(ASSETS_DIR, f"logo_{s}x{s}.png" if s != 512 else "logo.png")
with open(out_p, "wb") as f:
f.write(png_data)
print(f"[+] Saved PNG logo: {out_p}")
# 3. Generate Windows ICO File
ico_path = os.path.join(ASSETS_DIR, "logo.ico")
ico_data = generate_ico_file(png_buffers[:-1]) # Up to 256x256
with open(ico_path, "wb") as f:
f.write(ico_data)
print(f"[+] Saved Windows Icon (.ico): {ico_path}")
# 4. Generate NSIS / Inno Setup Wizard Header Bitmap (150x57) with seamless white background
header_bmp_path = os.path.join(ASSETS_DIR, "wizard_header.bmp")
header_bmp_data = create_bmp_24bit(150, 57, draw_wizard_header_white_bgr)
with open(header_bmp_path, "wb") as f:
f.write(header_bmp_data)
print(f"[+] Saved Wizard Header Bitmap (White Background): {header_bmp_path}")
# 5. Generate Inno Setup Small Wizard Bitmap (55x55) with seamless white background
small_bmp_path = os.path.join(ASSETS_DIR, "wizard_small.bmp")
small_bmp_data = create_bmp_24bit(55, 55, draw_wizard_small_bgr)
with open(small_bmp_path, "wb") as f:
f.write(small_bmp_data)
print(f"[+] Saved Small Wizard Bitmap (55x55): {small_bmp_path}")
# 6. Generate NSIS / Inno Setup Wizard Sidebar Bitmap (164x314)
sidebar_bmp_path = os.path.join(ASSETS_DIR, "wizard_sidebar.bmp")
sidebar_bmp_data = create_bmp_24bit(164, 314, draw_wizard_sidebar_bgr)
with open(sidebar_bmp_path, "wb") as f:
f.write(sidebar_bmp_data)
print(f"[+] Saved Wizard Sidebar Bitmap: {sidebar_bmp_path}")
print("[+] All LOGAR branding and installer graphics generated successfully.")
if __name__ == "__main__":
main()
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
HUB_URL="${1:-http://hub.example.com:8443}"
ENROLL_SECRET="${2:-}"
INSTALL_DIR="/opt/logar-client"
CONFIG_DIR="/etc/logar"
echo "[+] Installing LOGAR Client..."
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
chmod 700 "${CONFIG_DIR}/certs"
if [ -f "dist/Linux_Client.bin" ]; then
cp dist/Linux_Client.bin "${INSTALL_DIR}/Linux_Client"
elif [ -f "dist/Linux_Client" ]; then
cp dist/Linux_Client "${INSTALL_DIR}/Linux_Client"
else
echo "[!] Warning: dist/Linux_Client binary not found in current directory. Continuing with existing binary if present."
fi
if [ -f "${INSTALL_DIR}/Linux_Client" ]; then
chmod +x "${INSTALL_DIR}/Linux_Client"
fi
# Bootstrap certificate if missing and enrollment secret is provided
if [ ! -f "${CONFIG_DIR}/certs/client.crt" ] && [ -n "${ENROLL_SECRET}" ]; then
echo "[+] Enrolling client with LOGAR Hub..."
MACHINE_ID=$(cat /etc/machine-id 2>/dev/null || hostname)
RESPONSE=$(curl -s -X POST "${HUB_URL}/api/client/enroll" \
-H "Content-Type: application/json" \
-d "{\"client_id\": \"${MACHINE_ID}\", \"hostname\": \"$(hostname)\", \"os\": \"linux\", \"enrollment_secret\": \"${ENROLL_SECRET}\"}")
echo "${RESPONSE}" | grep -q "client_cert" || {
echo "[!] Enrollment failed: ${RESPONSE}"
exit 1
}
if command -v jq >/dev/null 2>&1; then
echo "${RESPONSE}" | jq -r .ca_cert > "${CONFIG_DIR}/certs/ca.crt"
echo "${RESPONSE}" | jq -r .client_cert > "${CONFIG_DIR}/certs/client.crt"
echo "${RESPONSE}" | jq -r .client_key > "${CONFIG_DIR}/certs/client.key"
else
python3 -c "import sys, json; data=json.loads(sys.stdin.read()); open('${CONFIG_DIR}/certs/ca.crt','w').write(data['ca_cert']); open('${CONFIG_DIR}/certs/client.crt','w').write(data['client_cert']); open('${CONFIG_DIR}/certs/client.key','w').write(data['client_key'])" <<< "${RESPONSE}"
fi
chmod 600 "${CONFIG_DIR}/certs/client.key"
echo "[+] Certificates written to ${CONFIG_DIR}/certs"
fi
cat <<EOF > /etc/systemd/system/logar-client.service
[Unit]
Description=LOGAR Edge Log Aggregator Client
After=network.target
[Service]
Type=simple
ExecStart=${INSTALL_DIR}/Linux_Client --config ${CONFIG_DIR}/config.json
Restart=always
RestartSec=5s
User=root
[Install]
WantedBy=multi-user.target
EOF
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload
systemctl enable --now logar-client.service || true
echo "[+] LOGAR Client service configured and activated."
else
echo "[+] Systemd service installed at /etc/systemd/system/logar-client.service"
fi
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
INSTALL_DIR="/opt/logar-server"
CONFIG_DIR="/etc/logar"
# 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"
if [ -f "dist/Server.bin" ]; then
cp dist/Server.bin "${INSTALL_DIR}/Server"
elif [ -f "dist/Server" ]; then
cp dist/Server "${INSTALL_DIR}/Server"
elif [ -f "dist/LOGAR_Server" ]; then
cp dist/LOGAR_Server "${INSTALL_DIR}/Server"
else
echo "[!] Warning: dist/Server.bin binary not found in current directory. Continuing with existing binary if present."
fi
if [ -f "${INSTALL_DIR}/Server" ]; then
chmod +x "${INSTALL_DIR}/Server"
fi
cat <<EOF > /etc/systemd/system/logar-server.service
[Unit]
Description=LOGAR Hub and Aggregator Engine
After=network.target
[Service]
Type=simple
WorkingDirectory=${INSTALL_DIR}
ExecStart=${INSTALL_DIR}/Server --config ${CONFIG_DIR}/server_config.json
Restart=always
RestartSec=5s
User=root
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload
systemctl enable --now logar-server.service || true
echo "[+] LOGAR Server service installed and activated."
else
echo "[+] Systemd service installed at /etc/systemd/system/logar-server.service"
fi
+178
View File
@@ -0,0 +1,178 @@
[Setup]
AppName=LOGAR Client
AppVersion=0.3.2
AppPublisher=LOGAR Security Systems
AppPublisherURL=https://logar.eibl.tech
AppSupportURL=https://logar.eibl.tech
AppUpdatesURL=https://logar.eibl.tech
DefaultDirName={autopf}\LOGAR
OutputDir=..\dist
OutputBaseFilename=LOGAR-Client-Setup
PrivilegesRequired=admin
Compression=lzma
SolidCompression=yes
WizardStyle=modern
SetupIconFile=assets\logo.ico
WizardImageFile=assets\wizard_sidebar.bmp
WizardSmallImageFile=assets\wizard_small.bmp
VersionInfoCompany=LOGAR Security Systems
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
VersionInfoVersion=0.3.2.0
VersionInfoProductName=LOGAR Client
VersionInfoProductVersion=0.3.2
[Files]
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs]
Name: "{code:GetDataDir}"; Permissions: users-modify
[Run]
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 AppStdout ""{code:GetDataDir}\client.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"""" --interval 30"""; Flags: runhidden
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Client"; Flags: runhidden
[UninstallRun]
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Client"; 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;
ConfigFilePage: TInputFileWizardPage;
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');
ConfigFilePage := CreateInputFilePage(
DataDirPage.ID,
'Import Client Configuration (Optional)',
'Do you have a pre-exported client_config.json profile to import?',
'Select your client_config.json profile exported from the LOGAR Server Hub.'#13#10#13#10 +
'If selected, this configuration will be copied into the data directory and used for automated server connection and enrollment.'#13#10#13#10 +
'You can also leave this blank to place client_config.json in the data directory manually later.'
);
ConfigFilePage.Add('Client Configuration (client_config.json):', 'JSON Files (*.json)|*.json|All Files (*.*)|*.*', '.json');
end;
function GetDataDir(Param: String): String;
begin
Result := DataDirPage.Values[0];
if Result = '' then
Result := ExpandConstant('{commonappdata}\LOGAR');
end;
function GetConfigFile(Param: String): String;
begin
Result := ConfigFilePage.Values[0];
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
CfgPath, DestCfg: String;
begin
if CurStep = ssInstall then
begin
Exec('net.exe', 'stop LOGAR_Client', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('sc.exe', 'stop LOGAR_Client', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM Win_Client.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM nssm.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Sleep(500);
end
else if CurStep = ssPostInstall then
begin
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
CfgPath := GetConfigFile('');
if (CfgPath <> '') and FileExists(CfgPath) then
begin
DestCfg := GetDataDir('') + '\client_config.json';
FileCopy(CfgPath, DestCfg, False);
end;
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
CustomDataDir, AppDir, PowerShellCmd: String;
I, ResultCode: Integer;
begin
if CurUninstallStep = usUninstall then
begin
AppDir := ExpandConstant('{app}');
Exec('net.exe', 'stop LOGAR_Client', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('sc.exe', 'stop LOGAR_Client', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if FileExists(AppDir + '\nssm.exe') then
begin
Exec(AppDir + '\nssm.exe', 'stop LOGAR_Client', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec(AppDir + '\nssm.exe', 'remove LOGAR_Client confirm', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
Exec('sc.exe', 'delete LOGAR_Client', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM Win_Client.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM nssm.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
PowerShellCmd := '-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "' +
'Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like ''' + AppDir + '\*'' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"';
Exec('powershell.exe', PowerShellCmd, '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Sleep(1200);
end
else if CurUninstallStep = usPostUninstall then
begin
AppDir := ExpandConstant('{app}');
Exec('taskkill.exe', '/F /T /IM Win_Client.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM nssm.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', CustomDataDir) then
begin
if (CustomDataDir <> '') and DirExists(CustomDataDir) then
begin
for I := 1 to 5 do
begin
if DelTree(CustomDataDir, True, True, True) or not DirExists(CustomDataDir) then
Break;
Sleep(300);
end;
end;
RegDeleteKeyIncludingSubkeys(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client');
end;
if (AppDir <> '') and DirExists(AppDir) then
begin
for I := 1 to 10 do
begin
DeleteFile(AppDir + '\nssm.exe');
DeleteFile(AppDir + '\Win_Client.exe');
DelTree(AppDir + '\runtime', True, True, True);
DelTree(AppDir + '\certs', True, True, True);
if DelTree(AppDir, True, True, True) or not DirExists(AppDir) then
Break;
Sleep(300);
end;
end;
end;
end;
+185
View File
@@ -0,0 +1,185 @@
!include "MUI2.nsh"
!include "LogicLib.nsh"
!include "nsDialogs.nsh"
Name "LOGAR Client"
OutFile "../dist/LOGAR-Client-Setup.exe"
InstallDir "$PROGRAMFILES64\LOGAR"
RequestExecutionLevel admin
BrandingText "LOGAR Centralized Log Aggregator & Forwarder"
VIProductVersion "0.3.2.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.2.0"
VIAddVersionKey "ProductVersion" "0.3.2.0"
# --- Visual Graphics & Branding ---
!define MUI_ICON "assets/logo.ico"
!define MUI_UNICON "assets/logo.ico"
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "assets/wizard_header.bmp"
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
Var DATADIR
Var CFGFILE
Var CfgDialog
Var CfgFileInput
Var CfgBrowseBtn
# --- 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
# Custom Page: Config File Selection
Page custom ConfigPageCreate ConfigPageLeave
# 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"
StrCpy $CFGFILE ""
FunctionEnd
Function ConfigPageCreate
nsDialogs::Create 1018
Pop $CfgDialog
${If} $CfgDialog == error
Abort
${EndIf}
!insertmacro MUI_HEADER_TEXT "Import Client Configuration (Optional)" "Choose a client_config.json profile exported from LOGAR Server."
${NSD_CreateLabel} 0 0 100% 30u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
Pop $0
${NSD_CreateText} 0 38u 75% 12u "$CFGFILE"
Pop $CfgFileInput
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
Pop $CfgBrowseBtn
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
nsDialogs::Show
FunctionEnd
Function OnBrowseCfg
nsDialogs::SelectFileDialog open "" "LOGAR Client Profiles (*.json)|*.json|All Files (*.*)|*.*"
Pop $0
${If} $0 != ""
StrCpy $CFGFILE $0
${NSD_SetText} $CfgFileInput $CFGFILE
${EndIf}
FunctionEnd
Function ConfigPageLeave
${NSD_GetText} $CfgFileInput $CFGFILE
FunctionEnd
Section "Install"
SetShellVarContext all
# Terminate any existing service or running processes before replacing files
ExecWait 'net stop LOGAR_Client'
ExecWait 'sc stop LOGAR_Client'
ExecWait 'taskkill /F /T /IM Win_Client.exe'
ExecWait 'taskkill /F /T /IM nssm.exe'
Sleep 500
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'
# If client config file was selected, copy to data directory
${If} $CFGFILE != ""
${AndIf} ${FileExists} "$CFGFILE"
CopyFiles /SILENT "$CFGFILE" "$DATADIR\client_config.json"
${EndIf}
# 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$\" --interval 30"'
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}
# 1. Stop and remove service
ExecWait 'net stop LOGAR_Client'
ExecWait 'sc stop LOGAR_Client'
${If} ${FileExists} "$INSTDIR\nssm.exe"
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm'
${EndIf}
ExecWait 'sc delete LOGAR_Client'
# 2. Force terminate any lingering processes
ExecWait 'taskkill /F /T /IM Win_Client.exe'
ExecWait 'taskkill /F /T /IM nssm.exe'
ExecWait `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process | Where-Object { $$_.ExecutablePath -like '$INSTDIR\*' } | ForEach-Object { Stop-Process -Id $$_.ProcessId -Force -ErrorAction SilentlyContinue }"`
Sleep 1200
# 3. Clean up all installation and data files
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"
DeleteRegKey /ifempty HKLM "Software\LOGAR"
SectionEnd
+155
View File
@@ -0,0 +1,155 @@
[Setup]
AppName=LOGAR Server
AppVersion=0.3.2
AppPublisher=LOGAR Security Systems
AppPublisherURL=https://logar.eibl.tech
AppSupportURL=https://logar.eibl.tech
AppUpdatesURL=https://logar.eibl.tech
DefaultDirName={autopf}\LOGAR-Server
OutputDir=..\dist
OutputBaseFilename=LOGAR-Server-Setup
PrivilegesRequired=admin
Compression=lzma
SolidCompression=yes
WizardStyle=modern
SetupIconFile=assets\logo.ico
WizardImageFile=assets\wizard_sidebar.bmp
WizardSmallImageFile=assets\wizard_small.bmp
VersionInfoCompany=LOGAR Security Systems
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
VersionInfoVersion=0.3.2.0
VersionInfoProductName=LOGAR Server
VersionInfoProductVersion=0.3.2
[Files]
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs]
Name: "{code:GetDataDir}"; Permissions: users-modify
[Run]
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 AppStdout ""{code:GetDataDir}\server.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
[UninstallRun]
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Server"; 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);
var
ResultCode: Integer;
begin
if CurStep = ssInstall then
begin
Exec('net.exe', 'stop LOGAR_Server', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('sc.exe', 'stop LOGAR_Server', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM Server.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM nssm.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Sleep(500);
end
else if CurStep = ssPostInstall then
begin
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
CustomDataDir, AppDir, PowerShellCmd: String;
I, ResultCode: Integer;
begin
if CurUninstallStep = usUninstall then
begin
AppDir := ExpandConstant('{app}');
Exec('net.exe', 'stop LOGAR_Server', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('sc.exe', 'stop LOGAR_Server', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if FileExists(AppDir + '\nssm.exe') then
begin
Exec(AppDir + '\nssm.exe', 'stop LOGAR_Server', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec(AppDir + '\nssm.exe', 'remove LOGAR_Server confirm', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
Exec('sc.exe', 'delete LOGAR_Server', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM Server.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM nssm.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
PowerShellCmd := '-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "' +
'Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like ''' + AppDir + '\*'' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"';
Exec('powershell.exe', PowerShellCmd, '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Sleep(1200);
end
else if CurUninstallStep = usPostUninstall then
begin
AppDir := ExpandConstant('{app}');
Exec('taskkill.exe', '/F /T /IM Server.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('taskkill.exe', '/F /T /IM nssm.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', CustomDataDir) then
begin
if (CustomDataDir <> '') and DirExists(CustomDataDir) then
begin
for I := 1 to 5 do
begin
if DelTree(CustomDataDir, True, True, True) or not DirExists(CustomDataDir) then
Break;
Sleep(300);
end;
end;
RegDeleteKeyIncludingSubkeys(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server');
end;
if (AppDir <> '') and DirExists(AppDir) then
begin
for I := 1 to 10 do
begin
DeleteFile(AppDir + '\nssm.exe');
DeleteFile(AppDir + '\Server.exe');
DelTree(AppDir + '\runtime', True, True, True);
DelTree(AppDir + '\certs', True, True, True);
if DelTree(AppDir, True, True, True) or not DirExists(AppDir) then
Break;
Sleep(300);
end;
end;
end;
end;
+136
View File
@@ -0,0 +1,136 @@
!include "MUI2.nsh"
!include "LogicLib.nsh"
Name "LOGAR Server"
OutFile "../dist/LOGAR-Server-Setup.exe"
InstallDir "$PROGRAMFILES64\LOGAR-Server"
RequestExecutionLevel admin
BrandingText "LOGAR Centralized Log Aggregator & Ingestion Hub"
VIProductVersion "0.3.2.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.2.0"
VIAddVersionKey "ProductVersion" "0.3.2.0"
# --- Visual Graphics & Branding ---
!define MUI_ICON "assets/logo.ico"
!define MUI_UNICON "assets/logo.ico"
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "assets/wizard_header.bmp"
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
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
# Terminate any existing service or running processes before replacing files
ExecWait 'net stop LOGAR_Server'
ExecWait 'sc stop LOGAR_Server'
ExecWait 'taskkill /F /T /IM Server.exe'
ExecWait 'taskkill /F /T /IM nssm.exe'
Sleep 500
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}
# 1. Stop and remove service
ExecWait 'net stop LOGAR_Server'
ExecWait 'sc stop LOGAR_Server'
${If} ${FileExists} "$INSTDIR\nssm.exe"
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm'
${EndIf}
ExecWait 'sc delete LOGAR_Server'
# 2. Force terminate any lingering processes
ExecWait 'taskkill /F /T /IM Server.exe'
ExecWait 'taskkill /F /T /IM nssm.exe'
ExecWait `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process | Where-Object { $$_.ExecutablePath -like '$INSTDIR\*' } | ForEach-Object { Stop-Process -Id $$_.ProcessId -Force -ErrorAction SilentlyContinue }"`
Sleep 1200
# 3. Clean up all installation and data files
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"
DeleteRegKey /ifempty HKLM "Software\LOGAR"
SectionEnd
Binary file not shown.
+411 -32
View File
@@ -12,14 +12,53 @@ SRC_DIR = os.path.join(ROOT_DIR, "src")
DIST_DIR = os.path.join(ROOT_DIR, "dist")
BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
def clean_and_prep():
if os.path.exists(DIST_DIR):
shutil.rmtree(DIST_DIR)
os.makedirs(DIST_DIR, exist_ok=True)
for item in os.listdir(DIST_DIR):
if item == "runtime":
continue
item_path = os.path.join(DIST_DIR, item)
try:
if os.path.isdir(item_path):
shutil.rmtree(item_path)
else:
os.remove(item_path)
except Exception:
pass
else:
os.makedirs(DIST_DIR, exist_ok=True)
if os.path.exists(BUILD_TEMP):
shutil.rmtree(BUILD_TEMP)
try:
shutil.rmtree(BUILD_TEMP)
except Exception:
pass
os.makedirs(BUILD_TEMP, exist_ok=True)
# Generate or verify branding images and icons
branding_script = os.path.join(ROOT_DIR, "compilation", "generate_branding_assets.py")
if os.path.exists(branding_script):
try:
subprocess.run([sys.executable, branding_script], cwd=os.path.join(ROOT_DIR, "compilation"), check=False)
except Exception:
pass
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):
print(f"[*] Compiling {binary_name} with PyInstaller...")
cmd = [
@@ -38,25 +77,15 @@ def build_pyinstaller_binary(script_path, binary_name):
raise RuntimeError(f"Failed to build {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():
print("[*] Packaging Linux standalone zipapp fallback binaries...")
# Linux Client zipapp
app_dir = os.path.join(BUILD_TEMP, "linux_app")
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")
zipapp.create_archive(
source=app_dir,
@@ -67,7 +96,9 @@ def build_linux_zipapp_fallback():
# Server zipapp
srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
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")
zipapp.create_archive(
source=srv_dir,
@@ -76,16 +107,297 @@ def build_linux_zipapp_fallback():
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")
build_pyinstaller_binary(server_script, "Server")
def prepare_windows_runtime():
"""
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")
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 = []
if os.path.exists(client_bin):
@@ -103,20 +415,33 @@ def build_windows():
else:
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")
with open(sums_path, "w", encoding="utf-8") as f:
f.write("\n".join(sums) + "\n")
def build_linux():
print("[*] Compiling Linux standalone binaries...")
print("[*] Compiling Linux standalone binaries and packaging installers...")
is_linux_host = platform.system() == "Linux"
if is_linux_host:
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py")
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
ensure_libpython()
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")
build_pyinstaller_binary(server_script, "Server.bin")
server_script = os.path.join(SRC_DIR, "Server.py")
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
for name in ["Linux_Client", "Server"]:
@@ -137,9 +462,37 @@ def build_linux():
print("[!] Note: Host platform is not Linux. Generating executable zipapps for Linux target.")
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")
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 = []
if os.path.exists(client_bin):
client_hash = calculate_sha256(client_bin)
@@ -156,16 +509,39 @@ def build_linux():
else:
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")
with open(sums_path, "w", encoding="utf-8") as f:
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):
if target is None:
parser = argparse.ArgumentParser(description="LOGAR Standalone Binary Compiler & Packager")
parser.add_argument(
"--target", "-t",
choices=["windows", "win", "linux", "auto"],
choices=["all", "windows", "win", "linux", "auto"],
default="auto",
help="Target platform to compile binaries for (default: auto-detect)"
)
@@ -173,19 +549,21 @@ def main(target=None):
target = args.target
if target == "auto":
target = "windows" if platform.system() == "Windows" else "linux"
target = "all"
elif target == "win":
target = "windows"
print("=" * 60)
print(" LOGAR Binary Packaging")
print(" LOGAR Binary Packaging & Cross-Compilation")
print(f" Host Platform: {platform.system()} ({platform.machine()})")
print(f" Target Platform: {target.upper()}")
print("=" * 60)
clean_and_prep()
if target == "windows":
if target == "all":
build_all()
elif target == "windows":
build_windows()
elif target == "linux":
build_linux()
@@ -202,5 +580,6 @@ def main(target=None):
print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)")
print("=" * 60)
if __name__ == "__main__":
main()
+2
View File
@@ -3,5 +3,7 @@ standard-imghdr>=3.13.0; python_version >= "3.13"
cryptography>=42.0.0
fastapi>=0.110.0
uvicorn>=0.28.0
websockets>=11.0.0
pydantic>=2.6.0
distlib>=0.3.8
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_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):
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:
with urllib.request.urlopen(req) as resp:
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):
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:
with urllib.request.urlopen(req) as resp:
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):
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("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):
url = f"{base_url}/api/v1/repos/{repo}/releases"
headers = {
"Authorization": f"token {token}",
"Authorization": get_auth_header(token),
"Content-Type": "application/json",
"Accept": "application/json"
}
# Check if release exists
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:
with urllib.request.urlopen(check_req) as resp:
existing = json.loads(resp.read().decode("utf-8"))
+49 -59
View File
@@ -1,40 +1,59 @@
# LOGAR Linux Edge Forwarder
Standalone compiled binary distribution for Linux edge servers running systemd.
Standalone compiled binary and automated systemd service distribution for Linux edge servers.
---
## Overview
`Linux_Client.bin` is a self-contained, pre-compiled executable that queries `systemd-journald` via `journalctl`, filters logs directly at the source, encrypts the payload using OpenPGP, and streams candidate events over an authenticated TCP socket to the central LOGAR hub.
`Linux_Client.bin` is a self-contained, pre-compiled executable that queries `systemd-journald` via `journalctl`, filters logs directly at the source, auto-enrolls with the central LOGAR hub, and streams candidate events over mutual TLS 1.3 (**mTLS**) to the central hub.
### Key Capabilities
- **Pre-compiled & Dependency-Free**: Ships as a standalone executable binary (`Linux_Client.bin`). No Python environment, pip packages, or GnuPG binaries are required on the host.
- **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR` (`journalctl -p info`). Drops debug noise (priority 7) and skips events older than 24 hours.
- **Pre-compiled & Dependency-Free**: Ships as a standalone native binary (`Linux_Client.bin`). No Python environment, pip packages, or GnuPG binaries are required on the host.
- **Mutual TLS 1.3 (mTLS) Ingestion**: Streams directly over hardware-authenticated TLS 1.3 sockets with machine-bound client certificates.
- **Automated Client Enrollment**: On first run with an `enrollment_secret`, the client automatically calls `POST /api/client/enroll` on the hub, saves its certificates into `/etc/logar/certs/`, and establishes secure mTLS streaming.
- **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.
- **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.
- **End-to-End Encryption**: Encrypts payloads using the server's OpenPGP public key before transmission.
- **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.
---
## 1. Generating & Deploying the Configuration File
## 1. Automated Installation via Script (Recommended)
Run the automated installer script:
```bash
sudo ./compilation/install_linux_client.sh "http://<HUB_HOST>:8443" "<ENROLLMENT_SECRET>"
```
This script:
1. Installs the binary to `/opt/logar-client/Linux_Client`.
2. Creates `/etc/logar/certs` with strict permissions.
3. Automatically queries `/etc/machine-id` and enrolls with the hub via `curl`.
4. Deploys, enables, and starts the systemd service unit `/etc/systemd/system/logar-client.service`.
---
## 2. Generating & Deploying the Configuration File
### Step 1: Generate `client_config.json` on the Server
Run the following command on your central LOGAR server to export a client bundle tailored for your environment:
Run the following command on your central LOGAR server:
```bash
python src/Server.py --create-client-config --server-host <SERVER_IP_OR_DNS> --server-port 9443 --client-out client_config.json
```
- Replace `<SERVER_IP_OR_DNS>` with the reachable IP address or FQDN of your central LOGAR server hub.
- Default TCP port is `9443`.
- Default mTLS socket port is `9443`; Hermes REST API port is `8443`.
### Step 2: Configuration Structure
The generated `client_config.json` contains:
```json
{
"server_host": "192.168.1.100",
"server_port": 9443,
"hermes_host": "192.168.1.100",
"hermes_port": 8443,
"enrollment_secret": "a1b2c3d4e5f6...",
"cert_dir": "certs",
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
"server_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"auth_token": "a1b2c3d4e5f6..."
@@ -42,11 +61,10 @@ The generated `client_config.json` contains:
```
> [!NOTE]
> A reference example is provided in `client_config.sample.json`. The configuration file contains **no host-specific names or site names** to ensure client anonymity and easy redistribution.
> The configuration contains **no host-specific names or site names** to ensure client anonymity and easy redistribution.
### Step 3: Copy to Edge Node
Place `Linux_Client.bin` and `client_config.json` into the target directory (recommended: `/opt/logar/`):
Place `Linux_Client.bin` and `client_config.json` into the target directory (e.g. `/opt/logar/`):
```bash
sudo mkdir -p /opt/logar
sudo cp Linux_Client.bin client_config.json /opt/logar/
@@ -55,14 +73,14 @@ sudo chmod +x /opt/logar/Linux_Client.bin
---
## 2. Running Manually
## 3. Running Manually
Test the forwarder interactively:
```bash
cd /opt/logar
./Linux_Client.bin --hours 24
```
On first run, the client contacts `http://<hermes_host>:<hermes_port>/api/client/enroll`, downloads `ca.crt`, `client.crt`, and `client.key` into `certs/`, and streams logs over mTLS.
### Command-Line Arguments
| Argument | Default | Description |
@@ -74,13 +92,10 @@ cd /opt/logar
---
## 3. Installing as a Systemd Service & Timer (Recommended)
Running `Linux_Client.bin` via a systemd timer ensures reliable periodic execution, automatic restart, and native log integration with `journalctl`.
## 4. Manual Systemd Service & Timer Setup
### Step 1: Create the Systemd Service Unit
Create `/etc/systemd/system/logar-forwarder.service`:
Create `/etc/systemd/system/logar-client.service`:
```ini
[Unit]
Description=LOGAR Edge Log Forwarder
@@ -88,9 +103,11 @@ After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
Type=simple
WorkingDirectory=/opt/logar
ExecStart=/opt/logar/Linux_Client.bin --hours 24
Restart=always
RestartSec=5s
User=root
StandardOutput=journal
StandardError=journal
@@ -99,51 +116,24 @@ StandardError=journal
WantedBy=multi-user.target
```
### Step 2: Create the Systemd Timer Unit
Create `/etc/systemd/system/logar-forwarder.timer` to execute the forwarder every 3 hours (with a 5-minute initial delay upon boot):
```ini
[Unit]
Description=Run LOGAR Edge Forwarder periodically
Requires=logar-forwarder.service
[Timer]
OnBootSec=5min
OnUnitActiveSec=3h
Persistent=true
[Install]
WantedBy=timers.target
```
### Step 3: Enable and Start the Timer
### Step 2: Enable and Start the Service
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now logar-forwarder.timer
sudo systemctl enable --now logar-client.service
```
### Step 4: Verify Timer & Service Status
### Step 3: Check Logs
```bash
# Check timer schedule
sudo systemctl list-timers --all | grep logar
# Trigger an immediate manual execution
sudo systemctl start logar-forwarder.service
# View execution logs
sudo journalctl -u logar-forwarder.service -n 50
sudo journalctl -u logar-client.service -n 50 -f
```
---
## 4. Alternative: Cron Job Deployment
If systemd timers are not preferred, configure a periodic cron job running every 3 hours:
## 5. Uninstallation & Removal
```bash
# Open root crontab
sudo crontab -e
# Add the following entry:
0 */3 * * * cd /opt/logar && ./Linux_Client.bin --hours 24 >> /var/log/logar_forwarder.log 2>&1
sudo systemctl disable --now logar-client.service
sudo rm -f /etc/systemd/system/logar-client.service
sudo systemctl daemon-reload
sudo rm -rf /opt/logar-client /opt/logar /etc/logar
```
+91 -139
View File
@@ -1,31 +1,47 @@
# LOGAR Linux Server Hub
Standalone compiled executable binary distribution for Linux server environments (`Server.bin`).
Standalone compiled binary and automated systemd service distribution for Linux server environments (`Server.bin`).
---
## Overview
`Server.bin` is a self-contained, pre-compiled Linux ELF executable that operates as the central coordination and log analysis hub of the LOGAR telemetry architecture.
`Server.bin` is a self-contained, pre-compiled Linux ELF executable that operates as the central coordination, log analysis, dynamic PKI, and reporting hub of the LOGAR telemetry architecture.
### Key Architecture & Capabilities
- **Pre-compiled & Dependency-Free**: Ships as a standalone native Linux ELF binary (`Server.bin`). No Python runtime, pip dependencies, or GnuPG binaries are required on the host system.
- **Authenticated TCP Ingestion Socket (Port 9443)**: Accepts framed OpenPGP encrypted log batches streamed by edge forwarders (`Linux_Client.bin` and `Win_Client.exe`).
- **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 are evaluated against an episodic threshold, requiring persistence across at least 4 distinct client transmission cycles within a sliding 12-hour evaluation window before promotion from transient noise to `VERIFIED`.
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external scrapers, SIEM collectors, and alerting dashboards.
- **Pure-Python OpenPGP Cryptography**: Zero dependency on external `gpg` binaries. Automatically generates RSA-2048 encryption keys and SHA-256 fingerprints on first launch.
- **State Database**: Tracks anomaly lifecycles, run counters, and machine telemetry in a local SQLite state database (`logar_state.db`).
- **Mutual TLS 1.3 (mTLS) Ingestion (Port 9443)**: Enforces mutual TLS 1.3 authentication for all incoming edge connections. Validates client certificates against an internal Subordinate CA and verifies active licensing in SQLite.
- **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).
- **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.
- **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`.
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
---
## 1. Initializing & Generating Server Configuration
## 1. Automated Installation via Script (Recommended)
Deploy using the automated installer:
```bash
sudo ./compilation/install_linux_server.sh
```
This script:
1. Installs the server binary to `/opt/logar-server/Server`.
2. Creates `/etc/logar` and `/var/log/logar`.
3. Deploys, enables, and starts the systemd service unit `/etc/systemd/system/logar-server.service` with `LimitNOFILE=65536`.
---
## 2. Initializing & Generating Server Configuration
### Step 1: Automatic First-Run Generation
When launched without an existing `server_config.json`, `Server.bin` automatically generates:
1. A fresh OpenPGP RSA-2048 encryption keypair (`private_key` and `public_key`).
2. A SHA-256 public encryption fingerprint (`server_fingerprint`).
3. A cryptographically random secret authentication token (`auth_token`).
4. Default network socket coordinates (TCP 9443, Hermes API 8443).
1. An internal Root CA (`certs/ca.crt` and `certs/ca.key`).
2. A server TLS certificate (`certs/server.crt` and `certs/server.key`) with SANs.
3. An OpenPGP RSA-2048 keypair (`private_key` and `public_key`).
4. Cryptographically random authentication tokens and enrollment secrets.
5. Default network socket coordinates (mTLS 9443, Hermes API 8443).
Run `Server.bin` once to initialize:
```bash
@@ -37,11 +53,20 @@ Output:
[+] Successfully generated new server config and OpenPGP keypair.
[+] Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
[+] Saved to: server_config.json
============================================================
LOGAR Server Hub: LOGAR-Cloud-Hub
Transport Security: mTLS (TLS 1.3)
License Quota: 10 Active Seats
Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
Evaluation Window: 12 hours | Direct Hermes Reasoning Enabled
============================================================
[*] 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
[*] Client Enrollment API available at http://0.0.0.0:8443/api/client/enroll
```
### Step 2: Configuration Fields Reference
The generated `server_config.json` contains:
```json
{
"server_name": "LOGAR-Linux-Hub",
@@ -50,159 +75,86 @@ The generated `server_config.json` contains:
"hermes_host": "0.0.0.0",
"hermes_port": 8443,
"auth_token": "a1b2c3d4e5f67890abcdef1234567890...",
"enrollment_secret": "e1f2a3b4c5d6...",
"max_seats": 10,
"cert_dir": "certs",
"tls_enabled": true,
"db_path": "logar_state.db",
"evaluation_window_hours": 12,
"min_persistence_runs": 4,
"min_persistence_runs": 1,
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
}
```
| Parameter | Default | Description |
| :--- | :--- | :--- |
| `server_name` | `"LOGAR-Linux-Hub"` | Human-readable identifier for this hub instance |
| `tcp_host` | `"0.0.0.0"` | Network interface to bind for edge client TCP ingestion |
| `tcp_port` | `9443` | TCP port for incoming edge log batches |
| `hermes_host` | `"0.0.0.0"` | Network interface to bind for Hermes HTTP API |
| `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint |
| `auth_token` | *(auto-generated)* | Pre-shared secret required in edge client envelopes |
| `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database |
| `evaluation_window_hours` | `12` | Sliding temporal window for warning persistence |
| `min_persistence_runs` | `4` | Number of distinct runs required to promote warnings to `VERIFIED` |
---
## 3. Exporting Client Configurations
Generate a client configuration bundle to deploy onto Windows or Linux forwarders:
```bash
./Server.bin --create-client-config --server-host 192.168.1.100 --server-port 9443 --client-out client_config.json
```
The output file contains the server coordinates, enrollment secret, and fingerprint, ready for client deployment.
---
## 2. Generating Client Configuration Bundles
## 4. Fleet Licensing, .lic Files & WebUI Dashboard
Edge forwarders (`Linux_Client.bin` and `Win_Client.exe`) require a minimal, anonymous configuration bundle containing socket coordinates, the authentication token, and the server's public key (without sensitive server names or private keys).
### Accessing the WebUI
Open your browser and navigate to:
```
http://<SERVER_IP>:8443/webui
```
The WebUI provides:
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`.
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. **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.
Run the following command on the server:
### CLI Licensing Utilities
Export a `.lic` request file via CLI:
```bash
./Server.bin --create-client-config --server-host <SERVER_PUBLIC_OR_INTERNAL_IP> --server-port 9443 --client-out client_config.json
./Server.bin --export-lic server.lic
```
Import an activated `.lic` license package or signed CA certificate via CLI:
```bash
./Server.bin --import-lic activated.lic
# Or alternatively with --import-ca:
./Server.bin --import-ca ca.crt
```
Synchronize license and check for seat quota increases against the master authority:
```bash
./Server.bin --sync-license
```
- Replace `<SERVER_PUBLIC_OR_INTERNAL_IP>` with the reachable IP or FQDN of your LOGAR server.
- The output `client_config.json` can be distributed directly to Linux and Windows edge forwarder nodes.
### 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.
---
## 3. Running Interactively
## 5. Manual Systemd Service Management
Check service status:
```bash
./Server.bin --config /path/to/server_config.json
```
### Command-Line Arguments
| Argument | Description |
| :--- | :--- |
| `--config` | Path to server configuration JSON file (default: `server_config.json`) |
| `--create-client-config` | Exports an anonymous client configuration bundle and exits |
| `--server-host` | Hostname/IP to embed in the exported client configuration |
| `--server-port` | Port to embed in the exported client configuration (default: `9443`) |
| `--client-out` | Destination path for exported client configuration (default: `client_config.json`) |
---
## 4. Installing as a Systemd Service (Recommended)
Running `Server.bin` as a native systemd background service ensures continuous execution, automatic restart upon reboot or crash, and centralized log management via `journalctl`.
### Step 1: Create Deployment Directory and User
```bash
# Create dedicated system group and user
sudo useradd --system --no-create-home --shell /usr/sbin/nologin logar
# Prepare deployment folder
sudo mkdir -p /opt/logar-server
sudo cp Server.bin server_config.json /opt/logar-server/
sudo chmod +x /opt/logar-server/Server.bin
sudo chown -R logar:logar /opt/logar-server
```
### Step 2: Create Systemd Service File
Create `/etc/systemd/system/logar-server.service`:
```ini
[Unit]
Description=LOGAR Central Server Hub Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=logar
Group=logar
WorkingDirectory=/opt/logar-server
ExecStart=/opt/logar-server/Server.bin --config /opt/logar-server/server_config.json
Restart=always
RestartSec=5
LimitNOFILE=65536
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
### Step 3: Enable and Start Service
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now logar-server.service
```
### Step 4: Verify Status and Inspect Logs
```bash
# Check service status
sudo systemctl status logar-server.service
```
# Stream live server logs
sudo journalctl -u logar-server.service -f
Inspect live service logs:
```bash
sudo journalctl -u logar-server.service -f -n 50
```
---
## 5. Hermes Reporting API & Integration
The server embeds a high-performance HTTP service on port `8443` providing real-time intelligence on promoted anomalies:
### Fetching Promoted Anomalies
```bash
curl -s http://127.0.0.1:8443/api/hermes/report | jq .
```
### Response Schema:
```json
[
{
"fingerprint": "prod-web-01.corp.internal:Out_Of_Memory",
"server": "prod-web-01.corp.internal",
"signature": "Out_Of_Memory",
"consecutive_runs": 4,
"first_seen": "2026-09-04T08:00:00Z",
"last_seen": "2026-09-04T14:30:00Z",
"status": "VERIFIED",
"verified": true,
"os_type": "linux",
"sample_message": "kernel: Out of memory: Kill process 1824"
}
]
```
---
## 6. Firewall Configuration
Ensure the following inbound ports are open on your host firewall:
## 6. Uninstallation & Removal
```bash
# UFW (Ubuntu / Debian)
sudo ufw allow 9443/tcp comment "LOGAR TCP Log Ingestion"
sudo ufw allow 8443/tcp comment "LOGAR Hermes Reporting API"
sudo ufw reload
# Firewalld (RHEL / CentOS / Rocky / Alma)
sudo firewall-cmd --permanent --add-port=9443/tcp
sudo firewall-cmd --permanent --add-port=8443/tcp
sudo firewall-cmd --reload
sudo systemctl disable --now logar-server.service
sudo rm -f /etc/systemd/system/logar-server.service
sudo systemctl daemon-reload
sudo rm -rf /opt/logar-server /etc/logar /var/log/logar
```
+52 -45
View File
@@ -1,40 +1,59 @@
# LOGAR Windows Edge Forwarder
Standalone compiled executable distribution for Windows Server and workstation environments.
Standalone compiled executable and installer distribution for Windows Server and workstation environments.
---
## Overview
`Win_Client.exe` is a self-contained, pre-compiled executable that queries the Windows Application Event Log, filters candidate events at the source, encrypts the payload using OpenPGP, and streams records over an authenticated TCP socket to the central LOGAR hub.
`Win_Client.exe` is a self-contained executable that queries the Windows Application Event Log, filters candidate events at the source, auto-enrolls with the central LOGAR hub to receive signed mTLS certificates, and streams records over mutual TLS 1.3 (**mTLS**) socket connection.
### Key Capabilities
- **Pre-compiled & Dependency-Free**: Ships as a standalone native Windows executable (`Win_Client.exe`). No Python installation, pip packages, or GnuPG binaries are required on the host.
- **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR`. Strips audit success/failure events and debug noise, skipping events older than 24 hours.
- **Pre-compiled & Dependency-Free**: Ships as a standalone native Windows executable (`Win_Client.exe`) or full installer (`LOGAR-Client-Setup.exe`). No Python installation, pip packages, or GnuPG binaries are required on the host.
- **Mutual TLS 1.3 (mTLS) Ingestion**: Streams directly over hardware-authenticated TLS 1.3 sockets with hardware/machine-bound client certificates.
- **Automated Client Enrollment**: On first run with an `enrollment_secret`, the client automatically calls `POST /api/client/enroll` on the hub, saves its certificates into `certs/`, and establishes secure mTLS streaming.
- **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.
- **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.
- **End-to-End Encryption**: Encrypts payloads using the server's OpenPGP public key before transmission.
- **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.
---
## 1. Generating & Deploying the Configuration File
## 1. Automated Installation via Inno Setup (Recommended)
Run the self-contained installer built from `compilation/installer_client.iss`:
```powershell
.\LOGAR-Client-Setup.exe
```
This installer:
1. Installs `Win_Client.exe` and bundled `nssm.exe` to `C:\Program Files\LOGAR\`.
2. Sets up directory permissions in `C:\ProgramData\LOGAR\`.
3. Registers and starts the `LOGAR_Client` Windows service automatically via NSSM.
4. Redirects stdout and stderr logs to `C:\ProgramData\LOGAR\client.log` and `client_err.log`.
---
## 2. Generating & Deploying the Configuration File
### Step 1: Generate `client_config.json` on the Server
Run the following command on your central LOGAR server to export a client bundle tailored for your environment:
Run the following command on your central LOGAR server:
```bash
python src/Server.py --create-client-config --server-host <SERVER_IP_OR_DNS> --server-port 9443 --client-out client_config.json
```
- Replace `<SERVER_IP_OR_DNS>` with the reachable IP address or FQDN of your central LOGAR server hub.
- Default TCP port is `9443`.
- Default mTLS socket port is `9443`; Hermes REST API port is `8443`.
### Step 2: Configuration Structure
The generated `client_config.json` contains:
```json
{
"server_host": "192.168.1.100",
"server_port": 9443,
"hermes_host": "192.168.1.100",
"hermes_port": 8443,
"enrollment_secret": "a1b2c3d4e5f6...",
"cert_dir": "certs",
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
"server_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"auth_token": "a1b2c3d4e5f6..."
@@ -42,11 +61,10 @@ The generated `client_config.json` contains:
```
> [!NOTE]
> A reference example is provided in `client_config.sample.json`. The configuration file contains **no host-specific names or site names** to ensure client anonymity and easy redistribution.
> The configuration contains **no host-specific names or site names** to ensure client anonymity and easy redistribution.
### Step 3: Copy to Edge Node
Place `Win_Client.exe` and `client_config.json` in the target directory (recommended: `C:\LOGAR\`):
Place `client_config.json` next to `Win_Client.exe` (e.g. `C:\Program Files\LOGAR\` or `C:\LOGAR\`):
```powershell
New-Item -ItemType Directory -Path "C:\LOGAR" -Force
Copy-Item "Win_Client.exe", "client_config.json" -Destination "C:\LOGAR\"
@@ -54,14 +72,14 @@ Copy-Item "Win_Client.exe", "client_config.json" -Destination "C:\LOGAR\"
---
## 2. Running Manually
## 3. Running Manually
Test the forwarder interactively from PowerShell or Command Prompt:
```powershell
cd C:\LOGAR
.\Win_Client.exe --hours 24
```
On first run, the client contacts `http://<hermes_host>:<hermes_port>/api/client/enroll`, downloads `ca.crt`, `client.crt`, and `client.key` into `certs/`, and streams logs over mTLS.
### Command-Line Arguments
| Argument | Default | Description |
@@ -73,22 +91,24 @@ cd C:\LOGAR
---
## 3. Installing as a Background Service / Scheduled Task
Edge forwarders run as episodic background processes (run, forward unsent candidate records, commit state, and terminate). On Windows, this is natively managed via Windows Task Scheduler running as a background service under `SYSTEM`.
### Method A: Windows Scheduled Task via PowerShell (Recommended)
Open an **Elevated PowerShell (Run as Administrator)** window and execute:
## 4. Manual Service Installation (NSSM or Scheduled Task)
### Method A: Windows Service via Bundled NSSM
```powershell
# From the compilation directory or with bundled nssm.exe:
.\nssm.exe install LOGAR_Client "C:\LOGAR\Win_Client.exe" "--hours 24"
.\nssm.exe set LOGAR_Client AppDirectory "C:\LOGAR"
.\nssm.exe set LOGAR_Client AppStdout "C:\ProgramData\LOGAR\client.log"
.\nssm.exe set LOGAR_Client AppStderr "C:\ProgramData\LOGAR\client_err.log"
.\nssm.exe start LOGAR_Client
```
### Method B: Windows Scheduled Task via PowerShell
```powershell
# Define action and periodic trigger (every 3 hours indefinitely)
$Action = New-ScheduledTaskAction -Execute "C:\LOGAR\Win_Client.exe" -Argument "--hours 24" -WorkingDirectory "C:\LOGAR"
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Hours 3)
# Configure task settings (wake on sleep, start when ready, run hidden)
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 15)
# Register task running under the local SYSTEM account with highest privileges
Register-ScheduledTask -TaskName "LOGAR_Forwarder" `
-Action $Action `
-Trigger $Trigger `
@@ -97,31 +117,18 @@ Register-ScheduledTask -TaskName "LOGAR_Forwarder" `
-RunLevel Highest `
-Description "LOGAR Windows Edge Log Forwarder Service"
# Verify task creation and trigger immediate execution
Start-ScheduledTask -TaskName "LOGAR_Forwarder"
Get-ScheduledTask -TaskName "LOGAR_Forwarder"
```
### Method B: Continuous Windows Service via NSSM
If your organizational policy requires a formal Windows Service listed under `services.msc`:
1. Download [NSSM (Non-Sucking Service Manager)](https://nssm.cc/).
2. Install the service using NSSM:
```cmd
nssm.exe install LOGAR_Forwarder "C:\LOGAR\Win_Client.exe" "--hours 24"
nssm.exe set LOGAR_Forwarder AppDirectory "C:\LOGAR"
nssm.exe set LOGAR_Forwarder AppRestartDelay 10800000
nssm.exe start LOGAR_Forwarder
```
*(Note: `AppRestartDelay 10800000` pauses 3 hours between execution cycles).*
---
## 4. Uninstallation & Removal
## 5. Uninstallation
To remove the scheduled task:
If installed via the Inno Setup installer, use **Windows Add/Remove Programs** or run `unins000.exe` in `C:\Program Files\LOGAR\`.
If installed manually via NSSM:
```powershell
Unregister-ScheduledTask -TaskName "LOGAR_Forwarder" -Confirm:$false
.\nssm.exe stop LOGAR_Client
.\nssm.exe remove LOGAR_Client confirm
Remove-Item -Recurse -Force "C:\LOGAR"
```
+91 -169
View File
@@ -1,31 +1,47 @@
# LOGAR Windows Server Hub
Standalone compiled executable distribution for Windows Server environments (`Server.exe`).
Standalone compiled executable and installer distribution for Windows Server environments (`Server.exe`).
---
## Overview
`Server.exe` is a self-contained, pre-compiled native Windows PE executable that serves as the central log aggregation, temporal persistence analyzer, and reporting hub of the LOGAR infrastructure.
`Server.exe` is a self-contained, pre-compiled native Windows PE executable that serves as the central log aggregation, temporal persistence analyzer, dynamic PKI certificate authority, and reporting hub of the LOGAR infrastructure.
### Key Architecture & Capabilities
- **Pre-compiled & Dependency-Free**: Ships as a standalone Windows executable (`Server.exe`). No Python installation, pip packages, or GnuPG binaries are required on Windows Server.
- **Authenticated TCP Ingestion Socket (Port 9443)**: Ingests framed OpenPGP encrypted log batches streamed from edge forwarder nodes (`Win_Client.exe` and `Linux_Client.bin`).
- **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 are evaluated against an episodic threshold, requiring persistence across at least 4 distinct client transmission cycles within a rolling 12-hour evaluation window before promotion to `VERIFIED`.
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external dashboards, monitoring agents, and scrapers.
- **Pure-Python OpenPGP Cryptography**: Automatically generates RSA-2048 encryption keys and a SHA-256 fingerprint on first launch without external dependencies.
- **State Database**: Stores issue lifecycle records, run counters, and machine telemetry in a local SQLite database (`logar_state.db`).
- **Pre-compiled & Dependency-Free**: Ships as a standalone Windows executable (`Server.exe`) or full installer (`LOGAR-Server-Setup.exe`). No Python installation, pip packages, or GnuPG binaries are required on Windows Server.
- **Mutual TLS 1.3 (mTLS) Ingestion (Port 9443)**: Enforces mutual TLS 1.3 authentication for all incoming edge connections. Validates client certificates against an internal Subordinate CA and verifies active licensing in SQLite.
- **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).
- **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.
- **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`.
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
---
## 1. Initializing & Generating Server Configuration
## 1. Automated Installation via Inno Setup (Recommended)
Run the self-contained installer built from `compilation/installer_server.iss`:
```powershell
.\LOGAR-Server-Setup.exe
```
This installer:
1. Installs `Server.exe` and bundled `nssm.exe` to `C:\Program Files\LOGAR-Server\`.
2. Registers and starts the `LOGAR_Server` Windows service automatically via NSSM.
3. Redirects stdout and stderr logs to `C:\ProgramData\LOGAR-Server\server.log` and `server_err.log`.
---
## 2. Initializing & Generating Server Configuration
### Step 1: Automatic First-Run Generation
When launched without an existing `server_config.json`, `Server.exe` automatically initializes:
1. An OpenPGP RSA-2048 encryption keypair (`private_key` and `public_key`).
2. A SHA-256 public encryption fingerprint (`server_fingerprint`).
3. A cryptographically random secret authentication token (`auth_token`).
4. Default network socket coordinates (TCP 9443, Hermes API 8443).
1. An internal Root CA (`certs/ca.crt` and `certs/ca.key`).
2. A server TLS certificate (`certs/server.crt` and `certs/server.key`) with SANs.
3. An OpenPGP RSA-2048 keypair (`private_key` and `public_key`).
4. Cryptographically random authentication tokens and enrollment secrets.
5. Default network socket coordinates (mTLS 9443, Hermes API 8443).
Open PowerShell and run:
```powershell
@@ -37,11 +53,20 @@ Output:
[+] Successfully generated new server config and OpenPGP keypair.
[+] Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
[+] Saved to: server_config.json
============================================================
LOGAR Server Hub: LOGAR-Cloud-Hub
Transport Security: mTLS (TLS 1.3)
License Quota: 10 Active Seats
Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
Evaluation Window: 12 hours | Direct Hermes Reasoning Enabled
============================================================
[*] 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
[*] Client Enrollment API available at http://0.0.0.0:8443/api/client/enroll
```
### Step 2: Configuration Fields Reference
The generated `server_config.json` contains:
```json
{
"server_name": "LOGAR-Windows-Hub",
@@ -50,188 +75,85 @@ The generated `server_config.json` contains:
"hermes_host": "0.0.0.0",
"hermes_port": 8443,
"auth_token": "a1b2c3d4e5f67890abcdef1234567890...",
"enrollment_secret": "e1f2a3b4c5d6...",
"max_seats": 10,
"cert_dir": "certs",
"tls_enabled": true,
"db_path": "logar_state.db",
"evaluation_window_hours": 12,
"min_persistence_runs": 4,
"min_persistence_runs": 1,
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
}
```
| Parameter | Default | Description |
| :--- | :--- | :--- |
| `server_name` | `"LOGAR-Windows-Hub"` | Identifier for this hub instance |
| `tcp_host` | `"0.0.0.0"` | Network interface to bind for incoming client socket traffic |
| `tcp_port` | `9443` | TCP port for incoming edge log batches |
| `hermes_host` | `"0.0.0.0"` | Network interface to bind for Hermes HTTP API |
| `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint |
| `auth_token` | *(auto-generated)* | Pre-shared authentication secret required in client envelopes |
| `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database |
| `evaluation_window_hours` | `12` | Rolling evaluation window in hours for warning persistence |
| `min_persistence_runs` | `4` | Consecutive runs required to promote warning issues to `VERIFIED` |
---
## 3. Exporting Client Configurations
Generate a client configuration bundle to deploy onto Windows or Linux forwarders:
```powershell
.\Server.exe --create-client-config --server-host 192.168.1.100 --server-port 9443 --client-out client_config.json
```
The output file contains the server coordinates, enrollment secret, and fingerprint, ready for client deployment.
---
## 2. Generating Client Configuration Bundles
## 4. Fleet Licensing, .lic Files & WebUI Dashboard
Edge forwarders (`Win_Client.exe` and `Linux_Client.bin`) require an anonymous client configuration bundle that includes the server socket target, authentication token, and encryption public key, without exposing sensitive server names or private keys.
### Accessing the WebUI
Open your browser and navigate to:
```
http://localhost:8443/webui
```
The WebUI provides:
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`.
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. **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.
Run the following command on the server:
### CLI Licensing Utilities
Export a `.lic` request file via CLI:
```powershell
.\Server.exe --create-client-config --server-host <SERVER_IP_OR_FQDN> --server-port 9443 --client-out client_config.json
.\Server.exe --export-lic C:\LOGAR-Server\server.lic
```
Import an activated `.lic` license package or signed CA certificate via CLI:
```powershell
.\Server.exe --import-lic C:\LOGAR-Server\activated.lic
# Or alternatively with --import-ca:
.\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
```
- Replace `<SERVER_IP_OR_FQDN>` with the reachable IP or DNS name of your LOGAR server.
- Distribute `client_config.json` to client forwarder nodes along with `Win_Client.exe` or `Linux_Client.bin`.
### 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.
---
## 3. Running Interactively
## 5. Manual Windows Service Setup (via NSSM)
```powershell
.\Server.exe --config C:\LOGAR-Server\server_config.json
```
### Command-Line Arguments
| Argument | Description |
| :--- | :--- |
| `--config` | Path to server configuration JSON file (default: `server_config.json`) |
| `--create-client-config` | Exports an anonymous client configuration bundle and exits |
| `--server-host` | Hostname/IP to embed in the exported client configuration |
| `--server-port` | Port to embed in the exported client configuration (default: `9443`) |
| `--client-out` | Destination path for exported client configuration (default: `client_config.json`) |
---
## 4. Installing as a Continuous Windows Service
Because `Server.exe` acts as a continuous server hub (listening for TCP connections and HTTP API queries), it should run persistently in the background.
### Method A: Native Windows Service via NSSM (Recommended)
[NSSM (Non-Sucking Service Manager)](https://nssm.cc/) is the industry standard for wrapping standalone executables into formal Windows services managed by `services.msc`.
1. Place `Server.exe` and `server_config.json` in `C:\LOGAR-Server\`.
2. Open **Elevated PowerShell (Run as Administrator)**:
```powershell
# Create deployment folder
New-Item -ItemType Directory -Path "C:\LOGAR-Server" -Force
Copy-Item "Server.exe", "server_config.json" -Destination "C:\LOGAR-Server\"
# Install Windows Service via NSSM
nssm.exe install LOGAR_Server "C:\LOGAR-Server\Server.exe" "--config C:\LOGAR-Server\server_config.json"
nssm.exe set LOGAR_Server AppDirectory "C:\LOGAR-Server"
nssm.exe set LOGAR_Server Description "LOGAR Central Aggregation Hub Service"
nssm.exe set LOGAR_Server Start SERVICE_AUTO_START
nssm.exe set LOGAR_Server AppStdout "C:\LOGAR-Server\server_out.log"
nssm.exe set LOGAR_Server AppStderr "C:\LOGAR-Server\server_err.log"
# Start the service
nssm.exe start LOGAR_Server
```
3. Verify status in PowerShell:
```powershell
Get-Service -Name "LOGAR_Server"
```
### Method B: Windows Task Scheduler (Startup Daemon)
If third-party service wrappers are restricted by organizational policy, configure a Task Scheduler job triggered at boot under the `SYSTEM` account:
```powershell
# Action: Launch Server.exe
$Action = New-ScheduledTaskAction -Execute "C:\LOGAR-Server\Server.exe" `
-Argument "--config C:\LOGAR-Server\server_config.json" `
-WorkingDirectory "C:\LOGAR-Server"
# Trigger: At system startup
$Trigger = New-ScheduledTaskTrigger -AtStartup
# Settings: Restart on failure, no execution time limit
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RestartCount 3 `
-RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero)
# Register task under SYSTEM with highest privileges
Register-ScheduledTask -TaskName "LOGAR_Server_Daemon" `
-Action $Action `
-Trigger $Trigger `
-Settings $Settings `
-User "NT AUTHORITY\SYSTEM" `
-RunLevel Highest `
-Description "LOGAR Central Hub Daemon"
# Start the task immediately
Start-ScheduledTask -TaskName "LOGAR_Server_Daemon"
Get-ScheduledTask -TaskName "LOGAR_Server_Daemon"
.\nssm.exe install LOGAR_Server "C:\LOGAR-Server\Server.exe"
.\nssm.exe set LOGAR_Server AppDirectory "C:\LOGAR-Server"
.\nssm.exe set LOGAR_Server AppStdout "C:\ProgramData\LOGAR-Server\server.log"
.\nssm.exe set LOGAR_Server AppStderr "C:\ProgramData\LOGAR-Server\server_err.log"
.\nssm.exe start LOGAR_Server
```
---
## 5. Hermes Reporting API & Health Checks
## 6. Uninstallation
Test the embedded Hermes REST endpoint locally using PowerShell:
If installed via the Inno Setup installer, use **Windows Add/Remove Programs**.
If installed manually via NSSM:
```powershell
$report = Invoke-RestMethod -Uri "http://127.0.0.1:8443/api/hermes/report" -Method GET
$report | Format-Table fingerprint, status, consecutive_runs, first_seen, last_seen
.\nssm.exe stop LOGAR_Server
.\nssm.exe remove LOGAR_Server confirm
```
### Response Format:
```json
[
{
"fingerprint": "win-dc-01.corp.internal:DiskCorruptionDetected",
"server": "win-dc-01.corp.internal",
"signature": "DiskCorruptionDetected",
"consecutive_runs": 4,
"first_seen": "2026-09-04T08:15:00Z",
"last_seen": "2026-09-04T15:00:00Z",
"status": "VERIFIED",
"verified": true,
"os_type": "windows",
"sample_message": "An error was detected on device \\Device\\Harddisk0\\DR0 during a paging operation."
}
]
```
---
## 6. Windows Defender Firewall Configuration
Open the necessary inbound firewall ports to allow incoming edge forwarder socket streams and HTTP API queries:
```powershell
# Allow TCP 9443 for edge log forwarding
New-NetFirewallRule -DisplayName "LOGAR TCP Log Ingestion" `
-Direction Inbound `
-LocalPort 9443 `
-Protocol TCP `
-Action Allow
# Allow TCP 8443 for Hermes Reporting REST API
New-NetFirewallRule -DisplayName "LOGAR Hermes Reporting API" `
-Direction Inbound `
-LocalPort 8443 `
-Protocol TCP `
-Action Allow
```
---
## 7. Uninstallation & Removal
To remove the server service:
```powershell
# If installed via NSSM:
nssm.exe stop LOGAR_Server
nssm.exe remove LOGAR_Server confirm
# If installed via Task Scheduler:
Unregister-ScheduledTask -TaskName "LOGAR_Server_Daemon" -Confirm:$false
# Clean files
Remove-Item -Recurse -Force "C:\LOGAR-Server"
```
+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
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""
LOGAR Master License Authority Proxy
Delegates directly to the standalone submodule: LOGAR-LICENSING/License_Authority.py
Repo: https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git
"""
import os
import importlib.util
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SUBMODULE_FILE = os.path.join(ROOT_DIR, "LOGAR-LICENSING", "License_Authority.py")
if not os.path.exists(SUBMODULE_FILE):
raise ImportError(f"LOGAR-LICENSING submodule not found at {SUBMODULE_FILE}. Run 'git submodule update --init --recursive'.")
spec = importlib.util.spec_from_file_location("_submodule_authority", SUBMODULE_FILE)
_sub = importlib.util.module_from_spec(spec)
spec.loader.exec_module(_sub)
# 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__":
_sub.main()
+85 -289
View File
@@ -1,298 +1,72 @@
"""
LOGAR Linux Edge Log Forwarder
Collects Linux systemd journalctl / syslog entries and securely streams them
to the central LOGAR Hub via mutual TLS (mTLS 1.3) or OpenPGP envelope.
"""
import os
import sys
import json
import socket
import struct
import time
import argparse
import subprocess
import warnings
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any, List
from typing import Optional
# Ensure script directory and repo root are in sys.path
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
if REPO_ROOT not in sys.path:
sys.path.insert(0, REPO_ROOT)
# Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore")
import pgpy
from log_collector import (
get_machine_identifier,
is_path_writable,
get_default_data_dir,
resolve_writable_path,
get_state_path,
load_state,
save_state,
commit_state,
LinuxLogCollector,
BaseLogCollector
)
from client_transport import (
is_cert_expiring_soon,
enroll_client_if_needed,
get_tls_socket,
load_config,
ensure_enrolled as transport_ensure_enrolled,
send_encrypted_logs_over_socket as transport_send_encrypted_logs
)
CONFIG_FILE_NAME = "client_config.json"
STATE_FILE_NAME = "client_state.json"
def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str:
if custom_state_path:
return custom_state_path
config_dir = os.path.dirname(os.path.abspath(config_path))
return os.path.join(config_dir, STATE_FILE_NAME)
def load_state(state_path: str) -> dict:
if os.path.exists(state_path):
try:
with open(state_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[!] Warning: Failed to read state file '{state_path}': {e}")
return {}
return {}
def save_state(state_path: str, state: dict):
try:
temp_path = f"{state_path}.tmp"
with open(temp_path, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
os.replace(temp_path, state_path)
except Exception as e:
print(f"[!] Warning: Could not save client state to '{state_path}': {e}")
def commit_state(state: dict, state_path: str):
if "new_last_cursor" in state:
val = state.pop("new_last_cursor")
if val:
state["last_cursor"] = val
if "new_last_timestamp_us" in state:
val = state.pop("new_last_timestamp_us")
if val:
state["last_timestamp_us"] = val
if "new_sent_cursors" in state:
state["sent_cursors"] = state.pop("new_sent_cursors")
save_state(state_path, state)
def load_config(config_path: str = CONFIG_FILE_NAME):
if not os.path.exists(config_path):
raise FileNotFoundError(
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}"
)
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
def get_machine_identifier() -> str:
"""
Returns the hostname of the machine sending the logs,
and appends the network/DNS domain if available.
"""
# 1. Try fully-qualified domain name (FQDN)
fqdn = socket.getfqdn()
if fqdn and "." in fqdn and not fqdn.startswith("localhost"):
return fqdn
hostname = socket.gethostname()
# 2. Check /etc/resolv.conf domain or search directive
try:
if os.path.exists("/etc/resolv.conf"):
with open("/etc/resolv.conf", "r", encoding="utf-8") as f:
for line in f:
parts = line.strip().split()
if parts and parts[0] in ["domain", "search"] and len(parts) > 1:
domain = parts[1]
if domain and not domain.startswith("."):
return f"{hostname}.{domain}"
except Exception:
pass
# 3. Try reverse DNS lookup
try:
host_ip = socket.gethostbyname(hostname)
canonical_name = socket.gethostbyaddr(host_ip)[0]
if canonical_name and "." in canonical_name and not canonical_name.startswith("localhost"):
return canonical_name
except Exception:
pass
return hostname
def get_recent_linux_logs(hours: int = 24, state: Optional[dict] = None) -> list:
"""
Collects info, warnings, and errors from systemd journalctl over the lookback window.
Edge Filtering: Retains INFO, WARNING, and ERROR. Strips DEBUG (priority 7) and skips events older than lookback window (default: 24h).
State Tracking: Skips events older than lookback window (default 24h) and events
that have already been sent in previous runs.
Edge Filtering: Retains INFO, WARNING, and ERROR. Strips DEBUG noise.
State Tracking: Skips events older than lookback window and already sent cursors.
"""
last_cursor = None
last_timestamp_us = 0.0
sent_cursors = set()
if state:
last_cursor = state.get("last_cursor")
try:
last_timestamp_us = float(state.get("last_timestamp_us", 0))
except (ValueError, TypeError):
last_timestamp_us = 0.0
sent_cursors = set(state.get("sent_cursors", []))
cmd = ["journalctl", "--since", f"{hours} hours ago", "-p", "info", "--output=json"]
result = None
if last_cursor:
cmd_with_cursor = ["journalctl", "--since", f"{hours} hours ago", "--after-cursor", str(last_cursor), "-p", "info", "--output=json"]
try:
res = subprocess.run(cmd_with_cursor, capture_output=True, text=True, check=False)
if res.returncode == 0:
result = res
except FileNotFoundError:
print("[!] journalctl command not found. Ensure this script runs on a systemd-enabled Linux system.")
return []
except Exception:
pass
if result is None:
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
except FileNotFoundError:
print("[!] journalctl command not found. Ensure this script runs on a systemd-enabled Linux system.")
return []
except Exception as e:
print(f"[!] Error running journalctl: {e}")
return []
logs = []
machine_id = get_machine_identifier()
cutoff_epoch_us = (datetime.now(timezone.utc) - timedelta(hours=hours)).timestamp() * 1_000_000
newest_cursor = None
newest_timestamp_us = last_timestamp_us
collected_cursors = []
for line in result.stdout.splitlines():
line_str = line.strip()
if not line_str:
continue
try:
entry = json.loads(line_str)
entry_cursor = entry.get("__CURSOR")
entry_ts_us_raw = entry.get("__REALTIME_TIMESTAMP")
entry_ts_us = 0.0
if entry_ts_us_raw:
try:
entry_ts_us = float(entry_ts_us_raw)
except (ValueError, TypeError):
pass
# 1. Skip entries older than lookback window (default: 24h)
if entry_ts_us and entry_ts_us < cutoff_epoch_us:
continue
# 2. Skip already sent events
if entry_cursor and (entry_cursor in sent_cursors or entry_cursor == last_cursor):
continue
if last_timestamp_us > 0 and entry_ts_us > 0 and entry_ts_us < last_timestamp_us:
continue
# Advance newest tracking for new entries
if entry_cursor:
newest_cursor = entry_cursor
collected_cursors.append(entry_cursor)
if entry_ts_us > newest_timestamp_us:
newest_timestamp_us = entry_ts_us
priority = int(entry.get("PRIORITY", "6"))
# Priority 0: Emerg, 1: Alert, 2: Crit, 3: Err (-> ERROR)
# Priority 4: Warning, 5: Notice (-> WARNING)
# Priority 6: Info (-> INFO)
# Priority 7: Debug (skip)
if priority > 6:
continue
if priority <= 3:
sev = "ERROR"
elif priority in (4, 5):
sev = "WARNING"
else:
sev = "INFO"
logs.append({
"server": machine_id,
"os_type": "linux",
"signature": entry.get("SYSLOG_IDENTIFIER", "unknown"),
"severity": sev,
"message": entry.get("MESSAGE", "")[:2048]
})
except (json.JSONDecodeError, ValueError):
continue
if state is not None:
state["new_last_cursor"] = newest_cursor or last_cursor
state["new_last_timestamp_us"] = max(newest_timestamp_us, last_timestamp_us)
state["new_sent_cursors"] = (list(sent_cursors) + collected_cursors)[-1000:]
state["last_run_timestamp"] = datetime.now(timezone.utc).isoformat()
return logs
collector = LinuxLogCollector(state=state)
return collector.collect_logs(hours=hours)
def send_encrypted_logs_over_socket(config: dict, logs: list):
"""
Encrypts the payload using the server's OpenPGP public key and streams
over an authenticated TCP socket.
"""
server_host = config["server_host"]
server_port = int(config["server_port"])
auth_token = config["auth_token"]
pub_key_armored = config["server_public_key"]
expected_fp = config.get("server_fingerprint", "").replace(" ", "").upper()
"""Streams logs to the LOGAR hub over mTLS or encrypted TCP envelope."""
return transport_send_encrypted_logs(config=config, logs=logs, os_type="linux")
# Load and verify server public key
pub_key, _ = pgpy.PGPKey.from_blob(pub_key_armored)
actual_fp = str(pub_key.fingerprint).replace(" ", "").upper()
if expected_fp and actual_fp != expected_fp:
raise ValueError(
f"Server fingerprint mismatch! Expected {expected_fp}, but key has {actual_fp}."
)
machine_id = get_machine_identifier()
# Prepare batch
payload = {
"server": machine_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"logs": logs
}
payload_json = json.dumps(payload)
# Encrypt payload with server's encryption-only key
pgp_msg = pgpy.PGPMessage.new(payload_json)
encrypted_msg = pub_key.encrypt(pgp_msg)
encrypted_armored = str(encrypted_msg)
# Envelope with socket authentication header
envelope = {
"auth_token": auth_token,
"timestamp": datetime.now(timezone.utc).isoformat(),
"encrypted_payload": encrypted_armored
}
envelope_bytes = json.dumps(envelope).encode("utf-8")
# Connect over TCP socket and transmit with 4-byte length prefix framing
print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over secure TCP socket...")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(15.0)
sock.connect((server_host, server_port))
# Send frame: length (4 bytes big-endian) + envelope
frame = struct.pack(">I", len(envelope_bytes)) + envelope_bytes
sock.sendall(frame)
# Receive response length
resp_len_bytes = sock.recv(4)
if not resp_len_bytes:
raise ConnectionError("Server closed connection without response.")
resp_len = struct.unpack(">I", resp_len_bytes)[0]
resp_bytes = bytearray()
while len(resp_bytes) < resp_len:
chunk = sock.recv(min(4096, resp_len - len(resp_bytes)))
if not chunk:
break
resp_bytes.extend(chunk)
response = json.loads(resp_bytes.decode("utf-8"))
print(f"[+] Server response: {response}")
return response
def ensure_enrolled(config: dict, machine_id: str) -> bool:
"""Proactively ensures client certificate enrollment with the Hub if enrollment_secret is available."""
return transport_ensure_enrolled(config=config, machine_id=machine_id, os_type="linux")
def main():
@@ -301,6 +75,7 @@ def main():
parser.add_argument("--hours", type=int, default=24, help="Lookback window in hours for journalctl logs (default: 24)")
parser.add_argument("--state-file", default=None, help="Path to state tracking file (default: client_state.json next to config)")
parser.add_argument("--no-state", action="store_true", help="Disable state tracking and send all events matching lookback window")
parser.add_argument("--interval", type=int, default=0, help="Continuous polling interval in seconds (0 = run once and exit, >0 = service loop)")
args = parser.parse_args()
try:
@@ -312,31 +87,52 @@ def main():
state_path = get_state_path(args.config, args.state_file)
state = None if args.no_state else load_state(state_path)
machine_id = get_machine_identifier()
machine_id = config.get("client_id") or get_machine_identifier()
print(f"[*] Edge Forwarder Node: {machine_id}")
if state and ("last_cursor" in state or "last_timestamp_us" in state):
print(f"[*] State tracking active: resuming after previous cursor/timestamp (state file: {state_path})")
elif not args.no_state:
print(f"[*] State tracking initialized (state file: {state_path})")
print(f"[*] Scanning Linux journalctl for unsent entries (last {args.hours} hours)...")
candidate_logs = get_recent_linux_logs(hours=args.hours, state=state)
print(f"[*] Found {len(candidate_logs)} unsent candidate entries (INFO to ERROR, entries > {args.hours}h and already-sent skipped).")
# Proactively ensure dynamic PKI certificate enrollment with Server Hub
ensure_enrolled(config, machine_id)
if not candidate_logs:
print("[*] No new unsent events to transmit.")
if state is not None:
commit_state(state, state_path)
return
collector = LinuxLogCollector(state=state, machine_id=machine_id)
try:
resp = send_encrypted_logs_over_socket(config, candidate_logs)
if state is not None and resp and resp.get("status") == "success":
commit_state(state, state_path)
print(f"[+] State successfully committed to {state_path}")
except Exception as e:
print(f"[!] Failed to stream logs to server: {e}")
sys.exit(1)
def run_forwarder_cycle():
# Continuously verify/renew PKI enrollment if server is licensed & online
ensure_enrolled(config, machine_id)
print(f"[*] Scanning Linux journalctl for unsent entries (last {args.hours} hours)...")
candidate_logs = collector.collect_logs(hours=args.hours)
print(f"[*] Found {len(candidate_logs)} unsent candidate entries (INFO to ERROR, entries > {args.hours}h and already-sent skipped).")
if not candidate_logs:
print("[*] No new unsent events to transmit.")
if state is not None:
commit_state(state, state_path)
return
try:
resp = send_encrypted_logs_over_socket(config, candidate_logs)
if state is not None and resp and resp.get("status") == "success":
commit_state(state, state_path)
print(f"[+] State successfully committed to {state_path}")
except Exception as e:
print(f"[!] Failed to stream logs to server: {e}")
if not args.interval:
sys.exit(1)
if args.interval and args.interval > 0:
print(f"[*] Starting continuous forwarder service loop (Interval: {args.interval}s)...")
while True:
try:
run_forwarder_cycle()
except Exception as e:
print(f"[!] Error in forwarder cycle: {e}")
time.sleep(args.interval)
else:
run_forwarder_cycle()
if __name__ == "__main__":
+2883 -136
View File
File diff suppressed because it is too large Load Diff
+144 -310
View File
@@ -1,310 +1,144 @@
import os
import sys
import json
import socket
import struct
import argparse
import warnings
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any, List
# Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore")
import pgpy
try:
import win32evtlog
except ImportError:
win32evtlog = None
CONFIG_FILE_NAME = "client_config.json"
STATE_FILE_NAME = "client_state.json"
def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str:
if custom_state_path:
return custom_state_path
config_dir = os.path.dirname(os.path.abspath(config_path))
return os.path.join(config_dir, STATE_FILE_NAME)
def load_state(state_path: str) -> dict:
if os.path.exists(state_path):
try:
with open(state_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[!] Warning: Failed to read state file '{state_path}': {e}")
return {}
return {}
def save_state(state_path: str, state: dict):
try:
temp_path = f"{state_path}.tmp"
with open(temp_path, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
os.replace(temp_path, state_path)
except Exception as e:
print(f"[!] Warning: Could not save client state to '{state_path}': {e}")
def commit_state(state: dict, state_path: str):
if "new_last_record_number" in state:
val = state.pop("new_last_record_number")
if val:
state["last_record_number"] = val
if "new_sent_record_ids" in state:
state["sent_record_ids"] = state.pop("new_sent_record_ids")
save_state(state_path, state)
def load_config(config_path: str = CONFIG_FILE_NAME):
if not os.path.exists(config_path):
raise FileNotFoundError(
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}"
)
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
def get_machine_identifier() -> str:
"""
Returns the hostname of the machine sending the logs,
and appends the network/DNS domain if available.
"""
fqdn = socket.getfqdn()
if fqdn and "." in fqdn and not fqdn.startswith("localhost"):
return fqdn
hostname = socket.gethostname()
user_dns_domain = os.environ.get("USERDNSDOMAIN")
if user_dns_domain and user_dns_domain.lower() != hostname.lower():
return f"{hostname}.{user_dns_domain.lower()}"
try:
host_ip = socket.gethostbyname(hostname)
canonical_name = socket.gethostbyaddr(host_ip)[0]
if canonical_name and "." in canonical_name and not canonical_name.startswith("localhost"):
return canonical_name
except Exception:
pass
return hostname
def get_recent_windows_logs(hours: int = 24, state: Optional[dict] = None) -> list:
"""
Scans the Windows Application Event Log backwards for events within the window.
Edge Filtering: Retains INFO, WARNING, and ERROR. Drops Audit and Debug noise.
State Tracking: Skips events older than lookback window (default 24h) and events
that have already been sent in previous runs.
"""
if win32evtlog is None:
print("[!] pywin32 is not installed or not running on Windows. Returning mock/empty candidate list.")
return []
server = "localhost"
log_type = "Application"
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
try:
hand = win32evtlog.OpenEventLog(server, log_type)
except Exception as e:
print(f"[!] Error opening Windows event log: {e}")
return []
logs = []
cutoff_time = datetime.now() - timedelta(hours=hours)
machine_id = get_machine_identifier()
last_record_number = 0
sent_record_ids = set()
if state:
last_record_number = int(state.get("last_record_number", 0))
sent_record_ids = set(state.get("sent_record_ids", []))
# Windows Event Log EventTypes:
# 1: EVENTLOG_ERROR_TYPE -> ERROR
# 2: EVENTLOG_WARNING_TYPE -> WARNING
# 4: EVENTLOG_INFORMATION_TYPE -> INFO
# Excludes: 8 (Audit Success), 16 (Audit Failure), and other verbose noise
sev_map = {
1: "ERROR",
2: "WARNING",
4: "INFO"
}
newest_record_number = 0
collected_record_ids = []
while True:
events = win32evtlog.ReadEventLog(hand, flags, 0)
if not events:
break
for event in events:
rec_num = int(event.RecordNumber)
if newest_record_number == 0:
newest_record_number = rec_num
# 1. Skip entries older than lookback window (default: 24h)
if event.TimeGenerated < cutoff_time:
break
# 2. Skip already sent events if we've reached records <= last_record_number
# (unless the log was cleared and numbers wrapped, i.e. newest_record_number < last_record_number)
if last_record_number > 0 and newest_record_number >= last_record_number:
if rec_num <= last_record_number:
break
rec_id = f"{rec_num}:{event.TimeGenerated.isoformat()}"
if rec_id in sent_record_ids:
continue
# Filter: upload everything from INFO to ERROR only
if event.EventType in sev_map:
msg = " ".join(event.StringInserts) if event.StringInserts else "Event Log Entry"
logs.append({
"server": machine_id,
"os_type": "windows",
"signature": event.SourceName or "Windows-Event",
"severity": sev_map[event.EventType],
"message": msg[:2048] # Limit message length
})
collected_record_ids.append(rec_id)
if events[-1].TimeGenerated < cutoff_time:
break
if last_record_number > 0 and newest_record_number >= last_record_number and events[-1].RecordNumber <= last_record_number:
break
win32evtlog.CloseEventLog(hand)
if state is not None:
target_rec = max(newest_record_number, last_record_number)
state["new_last_record_number"] = target_rec
state["new_sent_record_ids"] = (list(sent_record_ids) + collected_record_ids)[-1000:]
state["last_run_timestamp"] = datetime.now(timezone.utc).isoformat()
return logs
def send_encrypted_logs_over_socket(config: dict, logs: list):
"""
Encrypts the payload using the server's OpenPGP public key and streams
over an authenticated TCP socket. Zero local state is maintained on the client.
"""
server_host = config["server_host"]
server_port = int(config["server_port"])
auth_token = config["auth_token"]
pub_key_armored = config["server_public_key"]
expected_fp = config.get("server_fingerprint", "").replace(" ", "").upper()
# Load and verify server public key
pub_key, _ = pgpy.PGPKey.from_blob(pub_key_armored)
actual_fp = str(pub_key.fingerprint).replace(" ", "").upper()
if expected_fp and actual_fp != expected_fp:
raise ValueError(
f"Server fingerprint mismatch! Expected {expected_fp}, but key has {actual_fp}."
)
machine_id = get_machine_identifier()
# Prepare zero-state candidate batch
payload = {
"server": machine_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"logs": logs
}
payload_json = json.dumps(payload)
# Encrypt payload with server's encryption-only key
pgp_msg = pgpy.PGPMessage.new(payload_json)
encrypted_msg = pub_key.encrypt(pgp_msg)
encrypted_armored = str(encrypted_msg)
# Envelope with socket authentication header
envelope = {
"auth_token": auth_token,
"timestamp": datetime.now(timezone.utc).isoformat(),
"encrypted_payload": encrypted_armored
}
envelope_bytes = json.dumps(envelope).encode("utf-8")
# Connect over TCP socket and transmit with 4-byte length prefix framing
print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over secure TCP socket...")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(15.0)
sock.connect((server_host, server_port))
# Send frame: length (4 bytes big-endian) + envelope
frame = struct.pack(">I", len(envelope_bytes)) + envelope_bytes
sock.sendall(frame)
# Receive response length
resp_len_bytes = sock.recv(4)
if not resp_len_bytes:
raise ConnectionError("Server closed connection without response.")
resp_len = struct.unpack(">I", resp_len_bytes)[0]
resp_bytes = bytearray()
while len(resp_bytes) < resp_len:
chunk = sock.recv(min(4096, resp_len - len(resp_bytes)))
if not chunk:
break
resp_bytes.extend(chunk)
response = json.loads(resp_bytes.decode("utf-8"))
print(f"[+] Server response: {response}")
return response
def main():
parser = argparse.ArgumentParser(description="LOGAR Windows Edge Log Forwarder with State Tracking")
parser.add_argument("--config", default=CONFIG_FILE_NAME, help="Path to client_config.json")
parser.add_argument("--hours", type=int, default=24, help="Lookback window in hours for event logs (default: 24)")
parser.add_argument("--state-file", default=None, help="Path to state tracking file (default: client_state.json next to config)")
parser.add_argument("--no-state", action="store_true", help="Disable state tracking and send all events matching lookback window")
args = parser.parse_args()
try:
config = load_config(args.config)
except Exception as e:
print(f"[!] Configuration error: {e}")
sys.exit(1)
state_path = get_state_path(args.config, args.state_file)
state = None if args.no_state else load_state(state_path)
machine_id = get_machine_identifier()
print(f"[*] Edge Forwarder Node: {machine_id}")
if state and "last_record_number" in state:
print(f"[*] State tracking active: resuming from record #{state['last_record_number']} (state file: {state_path})")
elif not args.no_state:
print(f"[*] State tracking initialized (state file: {state_path})")
print(f"[*] Scanning Windows Application event log for unsent entries (last {args.hours} hours)...")
candidate_logs = get_recent_windows_logs(hours=args.hours, state=state)
print(f"[*] Found {len(candidate_logs)} unsent candidate entries (INFO to ERROR, entries > {args.hours}h and already-sent skipped).")
if not candidate_logs:
print("[*] No new unsent events to transmit.")
if state is not None:
commit_state(state, state_path)
return
try:
resp = send_encrypted_logs_over_socket(config, candidate_logs)
if state is not None and resp and resp.get("status") == "success":
commit_state(state, state_path)
print(f"[+] State successfully committed to {state_path}")
except Exception as e:
print(f"[!] Failed to stream logs to server: {e}")
sys.exit(1)
if __name__ == "__main__":
main()␍
"""
LOGAR Windows Edge Log Forwarder
Collects Windows Event Logs (Application/System) and securely streams them
to the central LOGAR Hub via mutual TLS (mTLS 1.3) or OpenPGP envelope.
"""
import os
import sys
import time
import argparse
import warnings
from typing import Optional
# Ensure script directory and repo root are in sys.path
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
if REPO_ROOT not in sys.path:
sys.path.insert(0, REPO_ROOT)
# Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore")
import pgpy
try:
import win32evtlog
except ImportError:
win32evtlog = None
from log_collector import (
get_machine_identifier,
is_path_writable,
get_default_data_dir,
resolve_writable_path,
get_state_path,
load_state,
save_state,
commit_state,
WindowsLogCollector,
BaseLogCollector
)
from client_transport import (
is_cert_expiring_soon,
enroll_client_if_needed,
get_tls_socket,
load_config,
ensure_enrolled as transport_ensure_enrolled,
send_encrypted_logs_over_socket as transport_send_encrypted_logs
)
CONFIG_FILE_NAME = "client_config.json"
STATE_FILE_NAME = "client_state.json"
def get_recent_windows_logs(hours: int = 24, state: Optional[dict] = None) -> list:
"""
Scans the Windows Application Event Log backwards for events within the window.
Edge Filtering: Retains INFO, WARNING, and ERROR. Drops Audit and Debug noise.
State Tracking: Skips events older than lookback window and already sent records.
"""
collector = WindowsLogCollector(state=state, channels=["Application"])
return collector.collect_logs(hours=hours)
def send_encrypted_logs_over_socket(config: dict, logs: list):
"""Streams logs to the LOGAR hub over mTLS or encrypted TCP envelope."""
return transport_send_encrypted_logs(config=config, logs=logs, os_type="windows")
def ensure_enrolled(config: dict, machine_id: str) -> bool:
"""Proactively ensures client certificate enrollment with the Hub if enrollment_secret is available."""
return transport_ensure_enrolled(config=config, machine_id=machine_id, os_type="windows")
def main():
parser = argparse.ArgumentParser(description="LOGAR Windows Edge Log Forwarder with State Tracking")
parser.add_argument("--config", default=CONFIG_FILE_NAME, help="Path to client_config.json")
parser.add_argument("--hours", type=int, default=24, help="Lookback window in hours for event logs (default: 24)")
parser.add_argument("--state-file", default=None, help="Path to state tracking file (default: client_state.json next to config)")
parser.add_argument("--no-state", action="store_true", help="Disable state tracking and send all events matching lookback window")
parser.add_argument("--interval", type=int, default=0, help="Continuous polling interval in seconds (0 = run once and exit, >0 = service loop)")
args = parser.parse_args()
try:
config = load_config(args.config)
except Exception as e:
print(f"[!] Configuration error: {e}")
sys.exit(1)
state_path = get_state_path(args.config, args.state_file)
state = None if args.no_state else load_state(state_path)
machine_id = config.get("client_id") or get_machine_identifier()
print(f"[*] Edge Forwarder Node: {machine_id}")
if state and "last_record_number" in state:
print(f"[*] State tracking active: resuming from record #{state['last_record_number']} (state file: {state_path})")
elif not args.no_state:
print(f"[*] State tracking initialized (state file: {state_path})")
# Proactively ensure dynamic PKI certificate enrollment with Server Hub
ensure_enrolled(config, machine_id)
collector = WindowsLogCollector(state=state, machine_id=machine_id, channels=["Application"])
def run_forwarder_cycle():
# Continuously verify/renew PKI enrollment if server is licensed & online
ensure_enrolled(config, machine_id)
print(f"[*] Scanning Windows Application event log for unsent entries (last {args.hours} hours)...")
candidate_logs = collector.collect_logs(hours=args.hours)
print(f"[*] Found {len(candidate_logs)} unsent candidate entries (INFO to ERROR, entries > {args.hours}h and already-sent skipped).")
if not candidate_logs:
print("[*] No new unsent events to transmit.")
if state is not None:
commit_state(state, state_path)
return
try:
resp = send_encrypted_logs_over_socket(config, candidate_logs)
if state is not None and resp and resp.get("status") == "success":
commit_state(state, state_path)
print(f"[+] State successfully committed to {state_path}")
except Exception as e:
print(f"[!] Failed to stream logs to server: {e}")
if not args.interval:
sys.exit(1)
if args.interval and args.interval > 0:
print(f"[*] Starting continuous forwarder service loop (Interval: {args.interval}s)...")
while True:
try:
run_forwarder_cycle()
except Exception as e:
print(f"[!] Error in forwarder cycle: {e}")
time.sleep(args.interval)
else:
run_forwarder_cycle()
if __name__ == "__main__":
main()
+322
View File
@@ -0,0 +1,322 @@
"""
LOGAR - Unified Client Transport & PKI Enrollment Module
Encapsulates mutual TLS (mTLS 1.3), OpenPGP fallback envelope framing,
and dynamic certificate enrollment with the LOGAR Server Hub.
"""
import os
import sys
import json
import socket
import ssl
import struct
import urllib.request
import warnings
from datetime import datetime, timezone, timedelta
from typing import Optional, List, Dict, Any
# Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore")
import pgpy
from log_collector import (
get_machine_identifier,
is_path_writable,
get_default_data_dir,
resolve_writable_path
)
CONFIG_FILE_NAME = "client_config.json"
def is_cert_expiring_soon(cert_path: str, threshold_days: int = 30) -> bool:
"""Checks if client certificate at cert_path is expiring within threshold_days."""
if not os.path.exists(cert_path):
return True
try:
from cryptography import x509
with open(cert_path, "r", encoding="utf-8") as f:
cert = x509.load_pem_x509_certificate(f.read().encode("utf-8"))
expiry = getattr(cert, "not_valid_after_utc", None)
if expiry is None:
expiry = cert.not_valid_after.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
return expiry <= (now + timedelta(days=threshold_days))
except Exception:
return True
def enroll_client_if_needed(
hub_url: str,
enrollment_secret: str,
cert_dir: str,
client_id: str,
hostname: str,
os_type: str = "windows",
force_renew: bool = False,
threshold_days: int = 30
) -> bool:
"""Bootstraps client enrollment if certificates are missing or expiring soon."""
os.makedirs(cert_dir, exist_ok=True)
ca_path = os.path.join(cert_dir, "ca.crt")
cert_path = os.path.join(cert_dir, "client.crt")
key_path = os.path.join(cert_dir, "client.key")
if not force_renew and os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path):
if not is_cert_expiring_soon(cert_path, threshold_days=threshold_days):
return True
print(f"[*] Client certificate at {cert_path} is expiring within {threshold_days} days. Auto-renewing...")
action_name = "re-enrolling" if os.path.exists(cert_path) else "enrolling"
print(f"[*] Bootstrapping client {action_name} with LOGAR Hub at {hub_url}...")
enroll_endpoint = f"{hub_url.rstrip('/')}/api/client/enroll"
payload = {
"client_id": client_id,
"hostname": hostname,
"os": os_type,
"enrollment_secret": enrollment_secret
}
req = urllib.request.Request(
enroll_endpoint,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status != 200:
raise RuntimeError(f"Enrollment failed with status code {resp.status}")
data = json.loads(resp.read().decode("utf-8"))
with open(ca_path, "w", encoding="utf-8") as f:
f.write(data["ca_cert"])
with open(cert_path, "w", encoding="utf-8") as f:
f.write(data["client_cert"])
with open(key_path, "w", encoding="utf-8") as f:
f.write(data["client_key"])
try:
os.chmod(key_path, 0o600)
except Exception:
pass
print(f"[+] Client certificates updated successfully in {os.path.abspath(cert_dir)}")
return True
def get_tls_socket(hub_host: str, hub_port: int, cert_dir: str):
"""Establishes an mTLS connection with the LOGAR hub using client certificates."""
ca_path = os.path.join(cert_dir, "ca.crt")
cert_path = os.path.join(cert_dir, "client.crt")
key_path = os.path.join(cert_dir, "client.key")
if not (os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path)):
raise FileNotFoundError(f"mTLS certificates not found in '{cert_dir}'. Enroll client first.")
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_path)
ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
ctx.check_hostname = False
raw_sock = socket.create_connection((hub_host, hub_port), timeout=15)
return ctx.wrap_socket(raw_sock, server_hostname=hub_host)
def load_config(config_path: str = CONFIG_FILE_NAME) -> dict:
"""Loads and normalizes client configuration JSON."""
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(
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}"
)
with open(actual_config_path, "r", encoding="utf-8") as f:
config = json.load(f)
# Resolve cert_dir to a writable directory
cfg_dir = os.path.dirname(os.path.abspath(actual_config_path))
raw_cert_dir = config.get("cert_dir", "certs")
if os.path.isabs(raw_cert_dir) and is_path_writable(raw_cert_dir):
config["cert_dir"] = raw_cert_dir
else:
candidate = os.path.join(cfg_dir, os.path.basename(raw_cert_dir))
if is_path_writable(candidate) or is_path_writable(cfg_dir):
config["cert_dir"] = candidate
else:
config["cert_dir"] = resolve_writable_path("certs", "LOGAR", "certs")
# If ca_cert is present in config and ca.crt doesn't exist yet, save it
ca_cert_pem = config.get("ca_cert")
if ca_cert_pem and isinstance(ca_cert_pem, str):
ca_file = os.path.join(config["cert_dir"], "ca.crt")
if not os.path.exists(ca_file):
try:
os.makedirs(config["cert_dir"], exist_ok=True)
with open(ca_file, "w", encoding="utf-8") as f:
f.write(ca_cert_pem)
except Exception:
pass
return config
def ensure_enrolled(config: dict, machine_id: str, os_type: str = "windows") -> bool:
"""Proactively ensures client certificate enrollment with the Hub if enrollment_secret is available."""
cert_dir = config.get("cert_dir", "certs")
enrollment_secret = config.get("enrollment_secret")
if not enrollment_secret:
return False
server_host = config.get("server_host", "127.0.0.1")
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)
hub_url = f"http://{hermes_host}:{hermes_port}"
try:
return enroll_client_if_needed(hub_url, enrollment_secret, cert_dir, machine_id, machine_id, os_type=os_type)
except Exception as e:
print(f"[!] Warning: Initial client enrollment bootstrap with Hub at {hub_url} failed: {e}")
return False
def send_encrypted_logs_over_socket(
config: dict,
logs: list,
os_type: Optional[str] = None,
machine_id: Optional[str] = None
) -> dict:
"""
Streams logs to the LOGAR hub.
Uses mutual TLS 1.3 (mTLS) with client certificates if available,
or falls back to OpenPGP encrypted envelope over TCP socket.
"""
node_os = os_type or ("windows" if sys.platform == "win32" else "linux")
server_host = config["server_host"]
server_port = int(config["server_port"])
cert_dir = config.get("cert_dir", "certs")
enrollment_secret = config.get("enrollment_secret")
actual_machine_id = machine_id or config.get("client_id") or get_machine_identifier()
# Attempt automatic enrollment bootstrap if certs are missing and secret is provided
hub_url = None
if enrollment_secret:
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)
hub_url = f"http://{hermes_host}:{hermes_port}"
try:
enroll_client_if_needed(hub_url, enrollment_secret, cert_dir, actual_machine_id, actual_machine_id, os_type=node_os)
except Exception as e:
print(f"[!] Warning: Enrollment bootstrap failed: {e}")
ca_path = os.path.join(cert_dir, "ca.crt")
cert_path = os.path.join(cert_dir, "client.crt")
key_path = os.path.join(cert_dir, "client.key")
has_mtls_certs = os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path)
if not has_mtls_certs and enrollment_secret and hub_url:
try:
enroll_client_if_needed(hub_url, enrollment_secret, cert_dir, actual_machine_id, actual_machine_id, os_type=node_os)
has_mtls_certs = os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path)
except Exception as e:
print(f"[!] Warning: Enrollment before TLS connection failed: {e}")
if has_mtls_certs:
print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over mTLS (TLS 1.3)...")
sock = None
try:
sock = get_tls_socket(server_host, server_port, cert_dir)
except (ssl.SSLError, ssl.CertificateError, ConnectionResetError) as tls_err:
if enrollment_secret and hub_url:
print(f"[!] TLS handshake error ({tls_err}). Re-enrolling with LOGAR Hub...")
try:
enroll_client_if_needed(hub_url, enrollment_secret, cert_dir, actual_machine_id, actual_machine_id, os_type=node_os, force_renew=True)
sock = get_tls_socket(server_host, server_port, cert_dir)
except Exception as retry_err:
print(f"[!] Re-enrollment or reconnection retry failed: {retry_err}")
raise
else:
raise
with sock:
payload = {
"server": actual_machine_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"logs": logs
}
payload_bytes = json.dumps(payload).encode("utf-8")
frame = struct.pack(">I", len(payload_bytes)) + payload_bytes
sock.sendall(frame)
resp_len_bytes = sock.recv(4)
if not resp_len_bytes:
raise ConnectionError("Server closed mTLS connection without response.")
resp_len = struct.unpack(">I", resp_len_bytes)[0]
resp_bytes = bytearray()
while len(resp_bytes) < resp_len:
chunk = sock.recv(min(4096, resp_len - len(resp_bytes)))
if not chunk:
break
resp_bytes.extend(chunk)
response = json.loads(resp_bytes.decode("utf-8"))
print(f"[+] Server response: {response}")
return response
# Fallback to OpenPGP envelope over plain TCP socket
auth_token = config.get("auth_token", "")
pub_key_armored = config.get("server_public_key")
if not pub_key_armored:
raise ValueError("No server public key or mTLS certificates available for connection.")
expected_fp = config.get("server_fingerprint", "").replace(" ", "").upper()
pub_key, _ = pgpy.PGPKey.from_blob(pub_key_armored)
actual_fp = str(pub_key.fingerprint).replace(" ", "").upper()
if expected_fp and actual_fp != expected_fp:
raise ValueError(f"Server fingerprint mismatch! Expected {expected_fp}, but key has {actual_fp}.")
payload = {
"server": actual_machine_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"logs": logs
}
payload_json = json.dumps(payload)
pgp_msg = pgpy.PGPMessage.new(payload_json)
encrypted_msg = pub_key.encrypt(pgp_msg)
encrypted_armored = str(encrypted_msg)
envelope = {
"auth_token": auth_token,
"timestamp": datetime.now(timezone.utc).isoformat(),
"encrypted_payload": encrypted_armored
}
envelope_bytes = json.dumps(envelope).encode("utf-8")
print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over secure TCP socket...")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(15.0)
sock.connect((server_host, server_port))
frame = struct.pack(">I", len(envelope_bytes)) + envelope_bytes
sock.sendall(frame)
resp_len_bytes = sock.recv(4)
if not resp_len_bytes:
raise ConnectionError("Server closed connection without response.")
resp_len = struct.unpack(">I", resp_len_bytes)[0]
resp_bytes = bytearray()
while len(resp_bytes) < resp_len:
chunk = sock.recv(min(4096, resp_len - len(resp_bytes)))
if not chunk:
break
resp_bytes.extend(chunk)
response = json.loads(resp_bytes.decode("utf-8"))
print(f"[+] Server response: {response}")
return response
+542
View File
@@ -0,0 +1,542 @@
"""
LOGAR - Unified Cross-Platform Log Collector & State Management
Provides modular log collection classes and state management for Windows and Linux,
reusable across Client Agents (Win_Client, Linux_Client) and Server Hubs (Windows & Linux).
"""
import os
import sys
import json
import socket
import subprocess
import warnings
from abc import ABC, abstractmethod
from datetime import datetime, timezone, timedelta
from typing import Optional, List, Dict, Any, Set
# Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore")
try:
import win32evtlog
except ImportError:
win32evtlog = None
CONFIG_FILE_NAME = "client_config.json"
STATE_FILE_NAME = "client_state.json"
def get_machine_identifier() -> str:
"""
Returns the fully qualified hostname of the machine sending/collecting logs.
Cross-platform resolution:
1. FQDN (if non-local)
2. Windows USERDNSDOMAIN (on Windows)
3. Linux /etc/resolv.conf domain/search directive (on Linux)
4. Reverse DNS lookup
5. socket.gethostname() fallback
"""
fqdn = socket.getfqdn()
if fqdn and "." in fqdn and not fqdn.startswith("localhost"):
return fqdn
hostname = socket.gethostname()
# Windows DNS domain
if sys.platform == "win32":
user_dns_domain = os.environ.get("USERDNSDOMAIN")
if user_dns_domain and user_dns_domain.lower() != hostname.lower():
return f"{hostname}.{user_dns_domain.lower()}"
# Linux resolv.conf search / domain
if sys.platform.startswith("linux") or os.path.exists("/etc/resolv.conf"):
try:
with open("/etc/resolv.conf", "r", encoding="utf-8") as f:
for line in f:
parts = line.strip().split()
if parts and parts[0] in ["domain", "search"] and len(parts) > 1:
domain = parts[1]
if domain and not domain.startswith("."):
return f"{hostname}.{domain}"
except Exception:
pass
# Reverse DNS lookup
try:
host_ip = socket.gethostbyname(hostname)
canonical_name = socket.gethostbyaddr(host_ip)[0]
if canonical_name and "." in canonical_name and not canonical_name.startswith("localhost"):
return canonical_name
except Exception:
pass
return hostname
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
else:
# Linux / Unix candidates: /var/lib/<app_name>, /tmp/<app_name>, ~/.local/share/<app_name>
var_lib = f"/var/lib/{app_name.lower()}"
if is_path_writable(var_lib):
return var_lib
user_share = os.path.expanduser(f"~/.local/share/{app_name.lower()}")
if is_path_writable(user_share):
return user_share
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:
if custom_state_path:
return custom_state_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)
def load_state(state_path: str) -> dict:
"""Loads state tracking dictionary from disk."""
if os.path.exists(state_path):
try:
with open(state_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[!] Warning: Failed to read state file '{state_path}': {e}")
return {}
return {}
def save_state(state_path: str, state: dict):
"""Atomically saves state dictionary to disk."""
try:
os.makedirs(os.path.dirname(os.path.abspath(state_path)), exist_ok=True)
temp_path = f"{state_path}.tmp"
with open(temp_path, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
os.replace(temp_path, state_path)
except Exception as e:
print(f"[!] Warning: Could not save client state to '{state_path}': {e}")
def commit_state(state: dict, state_path: str):
"""
Commits staged cursor/record state keys to persistent fields and writes to state_path.
Supports both Windows record numbers and Linux cursors/timestamps.
"""
if "new_last_record_number" in state:
val = state.pop("new_last_record_number")
if val:
state["last_record_number"] = val
if "new_sent_record_ids" in state:
state["sent_record_ids"] = state.pop("new_sent_record_ids")
if "new_last_cursor" in state:
val = state.pop("new_last_cursor")
if val:
state["last_cursor"] = val
if "new_last_timestamp_us" in state:
val = state.pop("new_last_timestamp_us")
if val:
state["last_timestamp_us"] = val
if "new_sent_cursors" in state:
state["sent_cursors"] = state.pop("new_sent_cursors")
save_state(state_path, state)
class BaseLogCollector(ABC):
"""
Abstract base class for platform-specific edge and hub log collectors.
Manages state persistence, machine identifiers, lookback windows, and normalization.
"""
def __init__(self, state: Optional[dict] = None, machine_id: Optional[str] = None):
self.state = state if state is not None else {}
self.machine_id = machine_id or get_machine_identifier()
@abstractmethod
def collect_logs(self, hours: int = 24) -> List[Dict[str, Any]]:
"""
Collects normalized logs within the given lookback window.
Returns a list of dicts:
[
{
"server": str,
"os_type": "windows"|"linux",
"signature": str,
"severity": "ERROR"|"WARNING"|"INFO",
"message": str
},
...
]
"""
pass
def commit(self, state_path: Optional[str] = None):
"""Commits staged state keys and optionally writes to disk."""
if state_path:
commit_state(self.state, state_path)
else:
if "new_last_record_number" in self.state:
val = self.state.pop("new_last_record_number")
if val:
self.state["last_record_number"] = val
if "new_sent_record_ids" in self.state:
self.state["sent_record_ids"] = self.state.pop("new_sent_record_ids")
if "new_last_cursor" in self.state:
val = self.state.pop("new_last_cursor")
if val:
self.state["last_cursor"] = val
if "new_last_timestamp_us" in self.state:
val = self.state.pop("new_last_timestamp_us")
if val:
self.state["last_timestamp_us"] = val
if "new_sent_cursors" in self.state:
self.state["sent_cursors"] = self.state.pop("new_sent_cursors")
class WindowsLogCollector(BaseLogCollector):
"""
Windows Event Log Collector.
Reads Application and System (or custom channels) backwards.
Edge Filtering: Retains INFO, WARNING, and ERROR. Drops Audit and Debug noise.
State Tracking: Skips events older than lookback window and already sent records.
"""
def __init__(
self,
state: Optional[dict] = None,
machine_id: Optional[str] = None,
channels: Optional[List[str]] = None
):
super().__init__(state=state, machine_id=machine_id)
self.channels = channels or ["Application"]
def collect_logs(self, hours: int = 24) -> List[Dict[str, Any]]:
if win32evtlog is None:
return []
logs = []
cutoff_time = datetime.now() - timedelta(hours=hours)
last_record_number = 0
sent_record_ids: Set[str] = set()
if self.state:
last_record_number = int(self.state.get("last_record_number", 0))
sent_record_ids = set(self.state.get("sent_record_ids", []))
# Windows Event Log EventTypes:
# 1: EVENTLOG_ERROR_TYPE -> ERROR
# 2: EVENTLOG_WARNING_TYPE -> WARNING
# 4: EVENTLOG_INFORMATION_TYPE -> INFO
sev_map = {
1: "ERROR",
2: "WARNING",
4: "INFO"
}
newest_record_number = 0
collected_record_ids = []
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
for log_type in self.channels:
try:
hand = win32evtlog.OpenEventLog("localhost", log_type)
except Exception as e:
continue
while True:
try:
events = win32evtlog.ReadEventLog(hand, flags, 0)
except Exception:
break
if not events:
break
for event in events:
rec_num = int(event.RecordNumber)
if newest_record_number == 0:
newest_record_number = rec_num
# 1. Skip entries older than lookback window
if event.TimeGenerated < cutoff_time:
break
# 2. Skip already sent events
if last_record_number > 0 and newest_record_number >= last_record_number:
if rec_num <= last_record_number:
break
rec_id = f"{log_type}:{rec_num}:{event.TimeGenerated.isoformat()}" if len(self.channels) > 1 else f"{rec_num}:{event.TimeGenerated.isoformat()}"
if rec_id in sent_record_ids:
continue
# Filter: upload everything from INFO to ERROR
if event.EventType in sev_map:
msg = " ".join(event.StringInserts) if event.StringInserts else "Event Log Entry"
sig = f"{log_type}/{event.SourceName or 'Windows-Event'}" if len(self.channels) > 1 else (event.SourceName or "Windows-Event")
logs.append({
"server": self.machine_id,
"os_type": "windows",
"signature": sig,
"severity": sev_map[event.EventType],
"message": msg[:2048]
})
collected_record_ids.append(rec_id)
if events[-1].TimeGenerated < cutoff_time:
break
if last_record_number > 0 and newest_record_number >= last_record_number and events[-1].RecordNumber <= last_record_number:
break
try:
win32evtlog.CloseEventLog(hand)
except Exception:
pass
if self.state is not None:
target_rec = max(newest_record_number, last_record_number)
self.state["new_last_record_number"] = target_rec
self.state["new_sent_record_ids"] = (list(sent_record_ids) + collected_record_ids)[-1000:]
# Also update direct keys for immediate server-side ingestion
self.state["last_record_number"] = target_rec
self.state["sent_record_ids"] = self.state["new_sent_record_ids"]
self.state["last_run_timestamp"] = datetime.now(timezone.utc).isoformat()
return logs
class LinuxLogCollector(BaseLogCollector):
"""
Linux Systemd Journal / Syslog Log Collector.
Collects info, warnings, and errors from journalctl (or syslog fallback).
Edge Filtering: Retains INFO, WARNING, and ERROR. Drops DEBUG noise.
State Tracking: Skips events older than lookback window and already sent cursors.
"""
def __init__(self, state: Optional[dict] = None, machine_id: Optional[str] = None):
super().__init__(state=state, machine_id=machine_id)
def collect_logs(self, hours: int = 24) -> List[Dict[str, Any]]:
last_cursor = None
last_timestamp_us = 0.0
sent_cursors: Set[str] = set()
if self.state:
last_cursor = self.state.get("last_cursor")
try:
last_timestamp_us = float(self.state.get("last_timestamp_us", 0))
except (ValueError, TypeError):
last_timestamp_us = 0.0
sent_cursors = set(self.state.get("sent_cursors", []))
cmd = ["journalctl", "--since", f"{hours} hours ago", "-p", "info", "--output=json"]
result = None
if last_cursor:
cmd_with_cursor = ["journalctl", "--since", f"{hours} hours ago", "--after-cursor", str(last_cursor), "-p", "info", "--output=json"]
try:
res = subprocess.run(cmd_with_cursor, capture_output=True, text=True, check=False)
if res.returncode == 0:
result = res
except Exception:
pass
if result is None:
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
except FileNotFoundError:
# journalctl not present, attempt fallback to /var/log/syslog or messages
return self._collect_syslog_fallback(hours=hours)
except Exception as e:
print(f"[!] Error running journalctl: {e}")
return []
if not result or result.returncode != 0:
return []
logs = []
cutoff_epoch_us = (datetime.now(timezone.utc) - timedelta(hours=hours)).timestamp() * 1_000_000
newest_cursor = None
newest_timestamp_us = last_timestamp_us
collected_cursors = []
for line in result.stdout.splitlines():
line_str = line.strip()
if not line_str:
continue
try:
entry = json.loads(line_str)
entry_cursor = entry.get("__CURSOR")
entry_ts_us_raw = entry.get("__REALTIME_TIMESTAMP")
entry_ts_us = 0.0
if entry_ts_us_raw:
try:
entry_ts_us = float(entry_ts_us_raw)
except (ValueError, TypeError):
pass
# 1. Skip entries older than lookback window
if entry_ts_us and entry_ts_us < cutoff_epoch_us:
continue
# 2. Skip already sent events
if entry_cursor and (entry_cursor in sent_cursors or entry_cursor == last_cursor):
continue
if last_timestamp_us > 0 and entry_ts_us > 0 and entry_ts_us < last_timestamp_us:
continue
# Advance tracking
if entry_cursor:
newest_cursor = entry_cursor
collected_cursors.append(entry_cursor)
if entry_ts_us > newest_timestamp_us:
newest_timestamp_us = entry_ts_us
priority = int(entry.get("PRIORITY", "6"))
if priority > 6:
continue
if priority <= 3:
sev = "ERROR"
elif priority in (4, 5):
sev = "WARNING"
else:
sev = "INFO"
logs.append({
"server": self.machine_id,
"os_type": "linux",
"signature": entry.get("SYSLOG_IDENTIFIER", "unknown"),
"severity": sev,
"message": entry.get("MESSAGE", "")[:2048]
})
except (json.JSONDecodeError, ValueError):
continue
if self.state is not None:
self.state["new_last_cursor"] = newest_cursor or last_cursor
self.state["new_last_timestamp_us"] = max(newest_timestamp_us, last_timestamp_us)
self.state["new_sent_cursors"] = (list(sent_cursors) + collected_cursors)[-1000:]
# Also update direct keys for immediate server-side ingestion
self.state["last_cursor"] = self.state["new_last_cursor"]
self.state["last_timestamp_us"] = self.state["new_last_timestamp_us"]
self.state["sent_cursors"] = self.state["new_sent_cursors"]
self.state["last_run_timestamp"] = datetime.now(timezone.utc).isoformat()
return logs
def _collect_syslog_fallback(self, hours: int = 24) -> List[Dict[str, Any]]:
"""Fallback log reader for non-systemd Linux environments."""
candidates = ["/var/log/syslog", "/var/log/messages"]
target = next((f for f in candidates if os.path.exists(f) and os.access(f, os.R_OK)), None)
if not target:
return []
logs = []
try:
with open(target, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()[-500:] # Read recent 500 lines
for line in lines:
line_str = line.strip()
if not line_str:
continue
sev = "INFO"
lower_l = line_str.lower()
if any(w in lower_l for w in ["err", "crit", "alert", "emerg", "fail"]):
sev = "ERROR"
elif any(w in lower_l for w in ["warn", "notice"]):
sev = "WARNING"
logs.append({
"server": self.machine_id,
"os_type": "linux",
"signature": "syslog",
"severity": sev,
"message": line_str[:2048]
})
except Exception:
pass
return logs
def get_log_collector(
os_type: Optional[str] = None,
state: Optional[dict] = None,
machine_id: Optional[str] = None,
channels: Optional[List[str]] = None
) -> BaseLogCollector:
"""
Factory helper that returns the appropriate BaseLogCollector instance.
If os_type is omitted, auto-detects based on sys.platform.
"""
target_os = os_type.lower() if os_type else ("windows" if sys.platform == "win32" else "linux")
if target_os == "windows":
chans = channels or (["Application", "System"] if channels is None else ["Application"])
return WindowsLogCollector(state=state, machine_id=machine_id, channels=chans)
else:
return LinuxLogCollector(state=state, machine_id=machine_id)
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
import warnings
# Suppress cryptography / pgpy deprecation notices during test runs
warnings.filterwarnings("ignore")
+233
View File
@@ -0,0 +1,233 @@
import os
import sys
import json
import shutil
import sqlite3
import unittest
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
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SRC_DIR = os.path.join(ROOT_DIR, "src")
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
if SRC_DIR not in sys.path:
sys.path.insert(0, SRC_DIR)
import server_enrollment as enrollment
import Server
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):
self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp")
os.makedirs(self.test_dir, exist_ok=True)
self.server_cert_dir = os.path.join(self.test_dir, "server_certs")
self.server_db = os.path.join(self.test_dir, "server_state.db")
def tearDown(self):
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir, ignore_errors=True)
def test_ca_csr_and_lic_request_generation(self):
"""Verifies subordinate CA CSR and .lic request file generation on the server."""
lic_data, lic_json = enrollment.create_license_request_lic(
cert_dir=self.server_cert_dir,
license_server="https://logar.eibl.tech",
server_fingerprint="AABBCCDDEEFF00112233",
hostname="test-hub.corp.internal",
requested_seats=30
)
self.assertEqual(lic_data["format"], "LOGAR_LICENSE_REQUEST")
self.assertEqual(lic_data["license_server"], "https://logar.eibl.tech")
self.assertEqual(lic_data["requested_seats"], 30)
self.assertIn("BEGIN CERTIFICATE REQUEST", lic_data["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")))
def test_license_info_extraction_and_custom_oids(self):
"""Verifies extraction of embedded custom seat OIDs and license attributes from certificates."""
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=42, license_id="LIC-ABC-4242")
info = enrollment.extract_license_info_from_cert(sub_pem)
self.assertTrue(info["valid"])
self.assertEqual(info["seats"], 42)
self.assertEqual(info["license_id"], "LIC-ABC-4242")
self.assertEqual(info["issuer_url"], "https://logar.eibl.tech")
self.assertFalse(info["is_expired"])
self.assertTrue(info["is_ca"])
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."""
# 1. Generate CSR on server side
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
# 2. Mock sign it with 50 seats
sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=50, license_id="LIC-IMPORT-50")
# 3. Setup mock server state and DB
Server.init_db(self.server_db, max_seats=5)
Server.SERVER_STATE["config"] = {
"db_path": self.server_db,
"cert_dir": self.server_cert_dir,
"max_seats": 5,
"tcp_host": "127.0.0.1",
"tcp_port": 9443,
"hermes_host": "127.0.0.1",
"hermes_port": 8443,
"server_fingerprint": "MOCK_FP",
"server_name": "Test-Hub",
"license_server_url": "https://logar.eibl.tech"
}
# 4. Extract summary before import
summary_before = Server.get_license_summary()
self.assertEqual(summary_before["max_seats"], 5)
# 5. Save the signed CA certificate to 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:
f.write(sub_pem)
# 6. Extract summary after import
summary_after = Server.get_license_summary()
self.assertEqual(summary_after["max_seats"], 50)
self.assertEqual(summary_after["license_id"], "LIC-IMPORT-50")
self.assertEqual(summary_after["status"], "ACTIVE")
def test_server_client_revocation_and_reactivation(self):
"""Verifies revocation and reactivation of edge client seats on the server hub."""
Server.init_db(self.server_db, max_seats=2)
Server.SERVER_STATE["config"] = {
"db_path": self.server_db,
"cert_dir": self.server_cert_dir,
"max_seats": 2
}
# Insert active clients
conn = sqlite3.connect(self.server_db)
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status) VALUES ('c1', 'win1', 'windows', 'FP1', 'active')")
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status) VALUES ('c2', 'lin1', 'linux', 'FP2', 'active')")
conn.commit()
conn.close()
# Revoke c1
req = Server.ClientActionRequest(client_id="c1")
res = Server.revoke_client(req)
self.assertEqual(res["status"], "revoked")
summary = Server.get_license_summary()
self.assertEqual(summary["active_seats"], 1)
self.assertEqual(summary["revoked_seats"], 1)
self.assertEqual(summary["available_seats"], 1)
# Reactivate c1
res_react = Server.reactivate_client(req)
self.assertEqual(res_react["status"], "active")
summary2 = Server.get_license_summary()
self.assertEqual(summary2["active_seats"], 2)
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):
"""Verifies that the WebUI HTML contains key components, stats, and download triggers."""
Server.init_db(self.server_db, max_seats=10)
Server.SERVER_STATE["config"] = {
"db_path": self.server_db,
"cert_dir": self.server_cert_dir,
"max_seats": 10,
"tcp_port": 9443,
"hermes_port": 8443,
"server_name": "Test-Cloud-Hub",
"server_fingerprint": "1234567890ABCDEF",
"license_server_url": "https://logar.eibl.tech"
}
response = Server.serve_webui()
html = response.body.decode("utf-8")
self.assertIn("LOGAR Fleet Hub", html)
self.assertNotIn("LOGAR Fleet Hub & License Management", html)
self.assertNotIn("Export .lic Request", html)
self.assertNotIn("Import License (.lic / ca.crt)", html)
self.assertIn("Sync & Renew via mTLS", html)
self.assertIn("logar.eibl.tech", html)
if __name__ == "__main__":
unittest.main()
+53 -1
View File
@@ -6,6 +6,7 @@ import unittest
import warnings
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__), "..", "src")))
@@ -26,7 +27,12 @@ class TestLinuxClientComponent(unittest.TestCase):
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
hashes=[HashAlgorithm.SHA256],
ciphers=[SymmetricKeyAlgorithm.AES256],
compression=[CompressionAlgorithm.Uncompressed]
compression=[
CompressionAlgorithm.ZIP,
CompressionAlgorithm.ZLIB,
CompressionAlgorithm.BZ2,
CompressionAlgorithm.Uncompressed
]
)
self.server_priv = key
self.server_pub = key.pubkey
@@ -198,6 +204,52 @@ class TestLinuxClientComponent(unittest.TestCase):
self.assertEqual(logs[0]["__CURSOR"], "c3")
self.assertEqual(newest_cursor, "c4")
def test_mtls_client_certificate_handling(self):
import shutil
test_dir = "test_linux_mtls_certs"
os.makedirs(test_dir, exist_ok=True)
try:
from src import server_enrollment as se
ca_cert, ca_key, ca_pem, _ = se.generate_ca_if_needed(test_dir)
client_cert_pem, client_key_pem = se.issue_client_cert("linux-client-test", ca_cert, ca_key)
with open(os.path.join(test_dir, "ca.crt"), "w") as f:
f.write(ca_pem)
with open(os.path.join(test_dir, "client.crt"), "w") as f:
f.write(client_cert_pem)
with open(os.path.join(test_dir, "client.key"), "w") as f:
f.write(client_key_pem)
# Test missing certs exception
empty_dir = "test_empty_linux_certs"
os.makedirs(empty_dir, exist_ok=True)
with self.assertRaises(FileNotFoundError):
Linux_Client.get_tls_socket("127.0.0.1", 9443, empty_dir)
shutil.rmtree(empty_dir, ignore_errors=True)
finally:
shutil.rmtree(test_dir, ignore_errors=True)
def test_client_certificate_validity_and_proactive_check(self):
import shutil
test_dir = "test_linux_client_validity"
os.makedirs(test_dir, exist_ok=True)
try:
from src import server_enrollment as se
ca_cert, ca_key, _, _ = se.generate_ca_if_needed(test_dir)
client_cert_pem, client_key_pem = se.issue_client_cert("linux-validity-test", ca_cert, ca_key, days_valid=365)
cert_path = os.path.join(test_dir, "client.crt")
with open(cert_path, "w", encoding="utf-8") as f:
f.write(client_cert_pem)
# Newly issued cert (365 days) is not expiring soon at 30 days
self.assertFalse(Linux_Client.is_cert_expiring_soon(cert_path, threshold_days=30))
# Large threshold (500 days) reports expiring soon
self.assertTrue(Linux_Client.is_cert_expiring_soon(cert_path, threshold_days=500))
# Non-existent file reports expiring / missing
self.assertTrue(Linux_Client.is_cert_expiring_soon(os.path.join(test_dir, "missing.crt")))
finally:
shutil.rmtree(test_dir, ignore_errors=True)
if __name__ == "__main__":
unittest.main()
+108
View File
@@ -0,0 +1,108 @@
"""
Unit tests for LOGAR log_collector and client_transport modules.
"""
import os
import sys
import json
import unittest
import warnings
from datetime import datetime, timezone, timedelta
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__), "..", "src")))
from log_collector import (
get_machine_identifier,
is_path_writable,
get_default_data_dir,
resolve_writable_path,
get_state_path,
load_state,
save_state,
commit_state,
BaseLogCollector,
WindowsLogCollector,
LinuxLogCollector,
get_log_collector
)
from client_transport import (
is_cert_expiring_soon,
load_config
)
class TestLogCollectorModule(unittest.TestCase):
def test_machine_identifier(self):
mid = get_machine_identifier()
self.assertIsInstance(mid, str)
self.assertTrue(len(mid) > 0)
self.assertNotEqual(mid, "localhost")
def test_path_writable_and_data_dir(self):
data_dir = get_default_data_dir("LOGAR_TEST")
self.assertIsInstance(data_dir, str)
self.assertTrue(is_path_writable(data_dir))
def test_state_lifecycle_windows_and_linux(self):
state_file = "test_shared_state.json"
try:
state = load_state(state_file)
self.assertEqual(state, {})
# Windows staged state
state["new_last_record_number"] = 100
state["new_sent_record_ids"] = ["100:2026-09-27T00:00:00"]
commit_state(state, state_file)
self.assertNotIn("new_last_record_number", state)
self.assertEqual(state["last_record_number"], 100)
# Linux staged state
state["new_last_cursor"] = "c_xyz"
state["new_last_timestamp_us"] = 123456789.0
state["new_sent_cursors"] = ["c_xyz"]
commit_state(state, state_file)
self.assertNotIn("new_last_cursor", state)
self.assertEqual(state["last_cursor"], "c_xyz")
self.assertEqual(state["last_timestamp_us"], 123456789.0)
# Reload and verify
reloaded = load_state(state_file)
self.assertEqual(reloaded["last_record_number"], 100)
self.assertEqual(reloaded["last_cursor"], "c_xyz")
finally:
if os.path.exists(state_file):
try:
os.remove(state_file)
except Exception:
pass
def test_windows_log_collector_severity_mapping(self):
state = {}
collector = WindowsLogCollector(state=state, machine_id="test-win-node", channels=["Application"])
self.assertEqual(collector.machine_id, "test-win-node")
self.assertEqual(collector.channels, ["Application"])
def test_linux_log_collector_priority_normalization(self):
state = {}
collector = LinuxLogCollector(state=state, machine_id="test-linux-node")
self.assertEqual(collector.machine_id, "test-linux-node")
def test_get_log_collector_factory(self):
win_col = get_log_collector(os_type="windows", machine_id="win-1")
self.assertIsInstance(win_col, WindowsLogCollector)
self.assertEqual(win_col.machine_id, "win-1")
lin_col = get_log_collector(os_type="linux", machine_id="lin-1")
self.assertIsInstance(lin_col, LinuxLogCollector)
self.assertEqual(lin_col.machine_id, "lin-1")
auto_col = get_log_collector()
self.assertIsInstance(auto_col, BaseLogCollector)
if __name__ == "__main__":
unittest.main()
+210 -54
View File
@@ -3,9 +3,12 @@ import sys
import json
import time
import socket
import ssl
import struct
import sqlite3
import urllib.request
import urllib.error
import shutil
import warnings
from datetime import datetime, timezone, timedelta
@@ -16,7 +19,9 @@ sys.path.insert(0, ROOT_DIR)
sys.path.insert(0, SRC_DIR)
warnings.filterwarnings("ignore")
import pgpy
import Win_Client
import Linux_Client
# Test server endpoints
TCP_HOST = "127.0.0.1"
@@ -24,9 +29,16 @@ TCP_PORT = 9443
HERMES_HOST = "127.0.0.1"
HERMES_PORT = 8443
def run_tests():
print("=== [1] Verifying server_config.json & client_config.json ===")
server_cfg_path = "server_config.json" if os.path.exists("server_config.json") else os.path.join(ROOT_DIR, "server_config.json")
server_cfg_path = "server_config.json"
if not os.path.exists(server_cfg_path):
server_cfg_path = os.path.join(ROOT_DIR, "server_config.json")
if not os.path.exists(server_cfg_path):
progdata_path = os.path.join(os.environ.get("ProgramData", r"C:\ProgramData"), "LOGAR-Server", "server_config.json")
if os.path.exists(progdata_path):
server_cfg_path = progdata_path
client_cfg_path = "client_config.json" if os.path.exists("client_config.json") else os.path.join(ROOT_DIR, "client_config.json")
assert os.path.exists(server_cfg_path), f"{server_cfg_path} must exist"
@@ -45,60 +57,198 @@ def run_tests():
print(f"[OK] Verified client_config.json contains no machine/server/site name.")
print(f"[OK] Fingerprint verified: {client_conf['server_fingerprint']}")
# Load public key
pub_key, _ = pgpy.PGPKey.from_blob(client_conf["server_public_key"])
cert_dir = os.path.join(ROOT_DIR, "test_pipeline_certs")
shutil.rmtree(cert_dir, ignore_errors=True)
os.makedirs(cert_dir, exist_ok=True)
client_id = "test-edge-node.corp.internal"
enrollment_secret = server_conf.get("enrollment_secret") or client_conf.get("enrollment_secret")
auth_token = server_conf.get("auth_token", "")
print("\n=== [1.5] Testing Server License Activation via Web API (/api/license/import) ===")
import server_enrollment
def send_socket_batch(logs, auth_token=client_conf["auth_token"]):
payload = {
"server": "test-edge-node.corp.internal",
"timestamp": datetime.now(timezone.utc).isoformat(),
"logs": logs
# 1. Check current license status
req_status_init = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/status")
with urllib.request.urlopen(req_status_init, timeout=5) as resp:
init_lic_stat = json.loads(resp.read().decode("utf-8"))
if init_lic_stat.get("status") == "UNLICENSED":
unlicensed_enroll_payload = {
"client_id": client_id,
"hostname": client_id,
"os": "linux",
"enrollment_secret": enrollment_secret
}
pgp_msg = pgpy.PGPMessage.new(json.dumps(payload))
enc = pub_key.encrypt(pgp_msg)
envelope = {
"auth_token": auth_token,
"timestamp": datetime.now(timezone.utc).isoformat(),
"encrypted_payload": str(enc)
}
envelope_bytes = json.dumps(envelope).encode("utf-8")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5.0)
s.connect((TCP_HOST, TCP_PORT))
frame = struct.pack(">I", len(envelope_bytes)) + envelope_bytes
req_unlicensed = urllib.request.Request(
f"http://{HERMES_HOST}:{HERMES_PORT}/api/client/enroll",
data=json.dumps(unlicensed_enroll_payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req_unlicensed, timeout=5):
assert False, "Expected HTTP 403 on unlicensed server enrollment"
except urllib.error.HTTPError as e:
assert e.code == 403, f"Expected HTTP 403 on unlicensed server, got {e.code}"
print("[OK] Unlicensed server correctly rejected edge client enrollment.")
# 2. Fetch CA CSR from Server
req_csr = urllib.request.Request(
f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/csr",
headers={"Authorization": f"Bearer {auth_token}"}
)
with urllib.request.urlopen(req_csr, timeout=5) as resp:
assert resp.status == 200
csr_data = json.loads(resp.read().decode("utf-8"))
csr_pem = csr_data["csr"]
print("[OK] Fetched subordinate CA CSR from server.")
# 3. Generate test master CA and sign subordinate CA with 10 seats
master_ca_dir = os.path.join(ROOT_DIR, "test_pipeline_master_ca")
master_cert, master_key, master_pem, master_key_pem = server_enrollment.generate_ca_if_needed(cert_dir=master_ca_dir, common_name="LOGAR-Master-Root-CA")
sub_pem = server_enrollment.sign_ca_csr_with_seats(
csr_pem=csr_pem,
master_ca_cert_pem=master_pem,
master_ca_key_pem=master_key_pem,
license_id="LIC-TEST-PIPELINE-10",
seats=10,
days_valid=365
)
# 4. Import signed license package to server
import_payload = {"ca_cert": sub_pem, "master_root_ca": master_pem}
req_import = urllib.request.Request(
f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/import",
data=json.dumps(import_payload).encode("utf-8"),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {auth_token}"}
)
with urllib.request.urlopen(req_import, timeout=5) as resp:
assert resp.status == 200
import_res = json.loads(resp.read().decode("utf-8"))
assert import_res["status"] == "success"
assert import_res["seats"] == 10
print("[OK] Successfully imported signed license (.lic). Server activated with 10 seats.")
# 5. Verify license status
req_status = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/status")
with urllib.request.urlopen(req_status, timeout=5) as resp:
assert resp.status == 200
lic_stat = json.loads(resp.read().decode("utf-8"))
assert lic_stat["status"] == "ACTIVE"
assert lic_stat["max_seats"] == 10
print("[OK] Verified server license status is ACTIVE.")
print("\n=== [2] Testing Client Dynamic PKI Enrollment API (/api/client/enroll) ===")
enroll_url = f"http://{HERMES_HOST}:{HERMES_PORT}/api/client/enroll"
# 2a. Test rejection on invalid enrollment secret
bad_enroll_payload = {
"client_id": client_id,
"hostname": client_id,
"os": "linux",
"enrollment_secret": "invalid-secret-xyz"
}
req_bad = urllib.request.Request(
enroll_url,
data=json.dumps(bad_enroll_payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req_bad, timeout=5):
assert False, "Expected HTTP 403 on invalid secret"
except urllib.error.HTTPError as e:
assert e.code == 403, f"Expected HTTP 403, got {e.code}"
print("[OK] Invalid enrollment secret rejected with HTTP 403.")
# 2b. Test valid client enrollment
valid_enroll_payload = {
"client_id": client_id,
"hostname": client_id,
"os": "linux",
"enrollment_secret": enrollment_secret
}
req_valid = urllib.request.Request(
enroll_url,
data=json.dumps(valid_enroll_payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req_valid, timeout=5) as resp:
assert resp.status == 200, f"Expected 200, got {resp.status}"
enroll_data = json.loads(resp.read().decode("utf-8"))
assert "ca_cert" in enroll_data
assert "client_cert" in enroll_data
assert "client_key" in enroll_data
ca_path = os.path.join(cert_dir, "ca.crt")
cert_path = os.path.join(cert_dir, "client.crt")
key_path = os.path.join(cert_dir, "client.key")
with open(ca_path, "w", encoding="utf-8") as f:
f.write(enroll_data["ca_cert"])
with open(cert_path, "w", encoding="utf-8") as f:
f.write(enroll_data["client_cert"])
with open(key_path, "w", encoding="utf-8") as f:
f.write(enroll_data["client_key"])
print(f"[OK] Client enrolled successfully. Certificates stored in {cert_dir}")
# 2c. Verify client shows in /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:
clients_data = json.loads(resp.read().decode("utf-8"))
assert clients_data["active_seats"] >= 1
found_c = any(c["client_id"] == client_id for c in clients_data["clients"])
assert found_c, f"Client {client_id} should be listed in /api/clients"
print(f"[OK] Verified client in /api/clients: Active Seats: {clients_data['active_seats']}/{clients_data['max_seats']}")
def send_mtls_batch(logs):
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_path)
ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
ctx.check_hostname = False
raw_sock = socket.create_connection((TCP_HOST, TCP_PORT), timeout=10)
with ctx.wrap_socket(raw_sock, server_hostname=TCP_HOST) as s:
payload = {
"server": client_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"logs": logs
}
payload_bytes = json.dumps(payload).encode("utf-8")
frame = struct.pack(">I", len(payload_bytes)) + payload_bytes
s.sendall(frame)
resp_len_bytes = s.recv(4)
if not resp_len_bytes:
raise ConnectionError("Server closed connection without response.")
resp_len = struct.unpack(">I", resp_len_bytes)[0]
resp_bytes = s.recv(resp_len)
resp_bytes = bytearray()
while len(resp_bytes) < resp_len:
chunk = s.recv(min(4096, resp_len - len(resp_bytes)))
if not chunk:
break
resp_bytes.extend(chunk)
return json.loads(resp_bytes.decode("utf-8"))
print("\n=== [2] Testing Socket Authentication Failure ===")
bad_resp = send_socket_batch([], auth_token="invalid-token-12345")
assert bad_resp.get("status") == "error", f"Expected error, got: {bad_resp}"
print(f"[OK] Bad auth rejected correctly: {bad_resp['message']}")
test_signature = "TestServiceDegraded"
print("\n=== [3] Testing Temporal Persistence & Direct Anomaly Verification over mTLS ===")
test_signature = f"TestServiceDegraded_{int(time.time())}"
candidate_log = [{
"server": "test-edge-node",
"server": client_id,
"os_type": "linux",
"signature": test_signature,
"severity": "WARNING",
"message": "Resource usage high warning"
}]
print("\n=== [3] Testing Temporal Persistence & 4-Run Rule for Warnings ===")
for run_num in range(1, 5):
resp = send_socket_batch(candidate_log)
resp = send_mtls_batch(candidate_log)
assert resp.get("status") == "success", f"Run {run_num} failed: {resp}"
promoted = resp.get("promoted_verified", 0)
print(f"[Run {run_num}/4] Ingested successfully. Promoted to verified: {promoted}")
if run_num < 4:
assert promoted == 0, f"Expected 0 promoted on run {run_num} for warning, got {promoted}"
print(f"[Run {run_num}/4] Ingested successfully via mTLS. Promoted to verified: {promoted}")
if run_num == 1:
assert promoted == 1, f"Expected 1 promoted on run 1 for warning, got {promoted}"
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
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
@@ -106,27 +256,27 @@ def run_tests():
cursor.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", (test_signature,))
row = cursor.fetchone()
conn.close()
assert row is not None, "Issue not found in SQLite"
run_count, status = row
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 status == "VERIFIED", f"Expected status 'VERIFIED', got {status}"
print("[OK] 4-Run Rule verified: Warning promoted to VERIFIED anomaly on 4th run!")
print("[OK] Direct verification confirmed: Warning immediately verified for Hermes with run count tracking over mTLS!")
print("\n=== [3b] Testing Immediate Pass for Errors ===")
error_signature = "TestServiceCrashImmediate"
print("\n=== [4] Testing Immediate Pass for Errors over mTLS ===")
error_signature = f"TestServiceCrashImmediate_{int(time.time())}"
error_log = [{
"server": "test-edge-node",
"server": client_id,
"os_type": "linux",
"signature": error_signature,
"severity": "ERROR",
"message": "Fatal process crash occurred"
}]
err_resp = send_socket_batch(error_log)
err_resp = send_mtls_batch(error_log)
assert err_resp.get("status") == "success", f"Error run failed: {err_resp}"
print(f"[Run 1/1] Error ingested successfully. Promoted to verified: {err_resp.get('promoted_verified')}")
assert err_resp.get("promoted_verified") == 1, f"Expected error to be promoted to verified immediately on run 1, got {err_resp.get('promoted_verified')}"
assert err_resp.get("promoted_verified") == 1, f"Expected error to be promoted to verified immediately, got {err_resp.get('promoted_verified')}"
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
cursor = conn.cursor()
@@ -140,12 +290,14 @@ def run_tests():
assert err_status == "VERIFIED", f"Expected status 'VERIFIED', got {err_status}"
print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!")
print("\n=== [4] Testing Hermes Reporting Endpoint (/api/hermes/report) ===")
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report")
print("\n=== [5] Testing Hermes Reporting Endpoint (/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:
assert response.status == 200, f"Expected 200, got {response.status}"
hermes_data = json.loads(response.read().decode("utf-8"))
print(f"[Hermes API] Returned {len(hermes_data)} verified anomalies:")
found_warning = False
found_error = False
@@ -164,19 +316,23 @@ def run_tests():
assert found_error, f"Error issue {error_signature} should be in Hermes report"
print("[OK] Hermes reporting validated!")
print("\n=== [5] Testing Windows Client Script Integration ===")
print("\n=== [6] Testing Windows Client Script Integration ===")
from Win_Client import get_recent_windows_logs
win_logs = get_recent_windows_logs(hours=24)
print(f"[Win_Client] Successfully queried Windows logs: {len(win_logs)} candidate entries.")
print("\n=== [6] Testing Linux Client Script Integration ===")
print("\n=== [7] Testing Linux Client Script Integration ===")
from Linux_Client import get_recent_linux_logs
linux_logs = get_recent_linux_logs(hours=24)
print(f"[Linux_Client] Successfully queried Linux logs: {len(linux_logs)} candidate entries.")
print("\n==========================================")
print(" ALL VERIFICATION TESTS PASSED SUCCESSFULLY! ")
print("==========================================")
if os.path.exists(cert_dir):
shutil.rmtree(cert_dir, ignore_errors=True)
print("\n=======================================================")
print(" ALL VERIFICATION TESTS (mTLS + PKI + PIPELINE) PASSED! ")
print("=======================================================")
if __name__ == "__main__":
run_tests()
+318 -22
View File
@@ -6,6 +6,7 @@ import struct
import sqlite3
import unittest
import urllib.request
import uuid
import warnings
from datetime import datetime, timezone, timedelta
@@ -21,12 +22,8 @@ import pgpy
class TestServerComponent(unittest.TestCase):
def setUp(self):
self.test_db = "test_server_state.db"
self.test_config = "test_server_config.json"
if os.path.exists(self.test_db):
os.remove(self.test_db)
if os.path.exists(self.test_config):
os.remove(self.test_config)
self.test_db = f"test_server_state_{uuid.uuid4().hex[:8]}.db"
self.test_config = f"test_server_config_{uuid.uuid4().hex[:8]}.json"
def tearDown(self):
if os.path.exists(self.test_db):
@@ -75,7 +72,7 @@ class TestServerComponent(unittest.TestCase):
if os.path.exists(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)
log_entry = {
"server": "app-worker-01.corp.local",
@@ -89,23 +86,24 @@ class TestServerComponent(unittest.TestCase):
"logs": [log_entry]
}
# Runs 1 to 3: WARNING should remain TRANSIENT
for run_idx in range(1, 4):
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
self.assertEqual(res["status"], "success")
self.assertEqual(res["promoted_verified"], 0)
# Run 1: WARNING is immediately VERIFIED for Hermes AI reasoning
res1 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
self.assertEqual(res1["status"], "success")
self.assertEqual(res1["promoted_verified"], 1)
conn = sqlite3.connect(self.test_db)
c = conn.cursor()
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
row = c.fetchone()
conn.close()
self.assertEqual(row[0], 3)
self.assertEqual(row[1], "TRANSIENT")
self.assertEqual(row[0], 1)
self.assertEqual(row[1], "VERIFIED")
# Run 4: promotes WARNING to VERIFIED!
res4 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
self.assertEqual(res4["promoted_verified"], 1)
# Runs 2 to 4: Increments consecutive run count while remaining VERIFIED
for run_idx in range(2, 5):
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)
c = conn.cursor()
@@ -130,7 +128,7 @@ class TestServerComponent(unittest.TestCase):
}
# 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["promoted_verified"], 1)
@@ -156,7 +154,7 @@ class TestServerComponent(unittest.TestCase):
{"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")
conn = sqlite3.connect(self.test_db)
@@ -171,11 +169,309 @@ class TestServerComponent(unittest.TestCase):
self.assertNotIn("SigDebug", 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["SigWarn"], "TRANSIENT")
self.assertEqual(rows["SigInfo"], "TRANSIENT")
self.assertEqual(rows["SigWarn"], "VERIFIED")
self.assertEqual(rows["SigInfo"], "VERIFIED")
def test_license_schema_and_pki_generation(self):
secret = "test-secret-12345"
Server.init_db(self.test_db, enrollment_secret=secret, max_seats=5)
conn = sqlite3.connect(self.test_db)
c = conn.cursor()
c.execute("SELECT max_seats, enrollment_secret FROM license_config WHERE id = 1")
row = c.fetchone()
conn.close()
self.assertIsNotNone(row)
self.assertEqual(row[0], 5)
self.assertEqual(row[1], secret)
# Test Dynamic PKI
test_cert_dir = "test_certs_pki"
try:
ca_cert, ca_key, ca_pem, ca_key_pem = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
self.assertIn("BEGIN CERTIFICATE", ca_pem)
self.assertIn("BEGIN RSA PRIVATE KEY", ca_key_pem)
srv_cert, srv_key, srv_pem, srv_key_pem = Server.enrollment.generate_server_cert_if_needed(
ca_cert, ca_key, hostnames=["127.0.0.1", "localhost"], cert_dir=test_cert_dir
)
self.assertIn("BEGIN CERTIFICATE", srv_pem)
client_cert_pem, client_key_pem = Server.enrollment.issue_client_cert("node-test-1", ca_cert, ca_key)
self.assertIn("BEGIN CERTIFICATE", client_cert_pem)
self.assertIn("BEGIN RSA PRIVATE KEY", client_key_pem)
fp = Server.enrollment.calculate_cert_fingerprint(client_cert_pem)
self.assertEqual(len(fp), 64)
finally:
import shutil
if os.path.exists(test_cert_dir):
shutil.rmtree(test_cert_dir, ignore_errors=True)
def test_enrollment_endpoint_and_seat_quota(self):
from fastapi import HTTPException
secret = "super-secret-enrollment"
max_seats = 2
Server.init_db(self.test_db, enrollment_secret=secret, max_seats=max_seats)
test_cert_dir = "test_certs_enroll"
try:
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": test_cert_dir}
Server.SERVER_STATE["ca_cert"] = ca_cert
Server.SERVER_STATE["ca_key"] = ca_key
Server.SERVER_STATE["ca_cert_pem"] = ca_pem
# 1. Invalid secret should raise 403
bad_req = Server.ClientEnrollRequest(
client_id="client-1",
hostname="host-1",
os="linux",
enrollment_secret="wrong-secret"
)
with self.assertRaises(HTTPException) as cm:
Server.enroll_client(bad_req)
self.assertEqual(cm.exception.status_code, 403)
# 2. Valid enrollment for client 1
req1 = Server.ClientEnrollRequest(
client_id="client-1",
hostname="host-1",
os="linux",
enrollment_secret=secret
)
resp1 = Server.enroll_client(req1)
self.assertIn("client_cert", resp1)
self.assertIn("client_key", resp1)
self.assertTrue(resp1["ca_cert"].strip().startswith(ca_pem.strip()))
# 3. Valid enrollment for client 2
req2 = Server.ClientEnrollRequest(
client_id="client-2",
hostname="host-2",
os="windows",
enrollment_secret=secret
)
resp2 = Server.enroll_client(req2)
self.assertIn("client_cert", resp2)
# 4. Seat quota exhausted: client 3 should raise 403
req3 = Server.ClientEnrollRequest(
client_id="client-3",
hostname="host-3",
os="linux",
enrollment_secret=secret
)
with self.assertRaises(HTTPException) as cm:
Server.enroll_client(req3)
self.assertEqual(cm.exception.status_code, 403)
self.assertIn("License seat limit reached", cm.exception.detail)
# 5. Re-enrollment for existing client 1 should succeed
resp1_re = Server.enroll_client(req1)
self.assertIn("client_cert", resp1_re)
# 6. Revoked client should be rejected
conn = sqlite3.connect(self.test_db)
conn.execute("UPDATE clients SET status = 'revoked' WHERE client_id = 'client-1'")
conn.commit()
conn.close()
with self.assertRaises(HTTPException) as cm:
Server.enroll_client(req1)
self.assertEqual(cm.exception.status_code, 403)
self.assertIn("revoked", cm.exception.detail)
finally:
import shutil
if os.path.exists(test_cert_dir):
shutil.rmtree(test_cert_dir, ignore_errors=True)
def test_cert_validity_and_hub_pki_renewal(self):
test_cert_dir = "test_certs_renew"
try:
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
srv_cert, srv_key, srv_pem, _ = Server.enrollment.generate_server_cert_if_needed(
ca_cert, ca_key, hostnames=["127.0.0.1"], cert_dir=test_cert_dir
)
# 1. Freshly generated certificates should NOT be expiring soon with standard 30-day threshold
self.assertFalse(Server.enrollment.is_cert_expiring_soon(ca_pem, threshold_days=30))
self.assertFalse(Server.enrollment.is_cert_expiring_soon(srv_pem, threshold_days=30))
# 2. Huge threshold (e.g. 5000 days) should flag expiration
self.assertTrue(Server.enrollment.is_cert_expiring_soon(srv_pem, threshold_days=5000))
# 3. check_and_renew_hub_pki with standard threshold should report no renewal needed
ca_renewed, srv_renewed = Server.enrollment.check_and_renew_hub_pki(cert_dir=test_cert_dir, threshold_days=30)
self.assertFalse(ca_renewed)
self.assertFalse(srv_renewed)
# 4. In-flight reload of SSLContext
ssl_ctx = Server.init_mtls_server_context(cert_dir=test_cert_dir)
Server.SERVER_STATE["ssl_ctx"] = ssl_ctx
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": test_cert_dir, "tcp_host": "127.0.0.1"}
# Trigger rotation using high threshold
rotated = Server.check_and_rotate_server_certs(cert_dir=test_cert_dir, hostnames=["127.0.0.1"], threshold_days=5000)
self.assertTrue(rotated)
# Check that backup files were generated
bak_files = [f for f in os.listdir(test_cert_dir) if f.endswith(".bak")]
self.assertGreater(len(bak_files), 0)
finally:
import shutil
if os.path.exists(test_cert_dir):
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")
now_fresh = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
rel = Server.format_relative_time(now_fresh)
self.assertTrue(rel.endswith("s ago") or rel == "Just now", f"Unexpected relative time: {rel}")
def test_integrated_local_log_collector_zero_seats(self):
"""Tests that local hub log collection runs without license, registering a 0-seat built-in client."""
Server.init_db(self.test_db, enrollment_secret="test-sec-456", max_seats=0)
Server.SERVER_STATE["config"] = {
"db_path": self.test_db,
"evaluation_window_hours": 12,
"min_persistence_runs": 1
}
# Run cycle
state = {}
processed = Server.run_local_log_collection_cycle(self.test_db, window_hours=12, min_runs=1, state=state)
# Verify local hub client entry exists in clients table
machine_id = Server.get_machine_identifier()
conn = sqlite3.connect(self.test_db)
c = conn.cursor()
c.execute("SELECT client_id, cert_fingerprint, status FROM clients WHERE client_id = ?", (machine_id,))
row = c.fetchone()
conn.close()
self.assertIsNotNone(row)
self.assertEqual(row[0], machine_id)
self.assertEqual(row[1], "LOCAL-HOST-BUILTIN")
self.assertEqual(row[2], "active")
# Verify active seat count is STILL 0 (exempt from seat accounting)
client_data = Server.list_clients()
self.assertEqual(client_data["active_seats"], 0)
self.assertEqual(len(client_data["clients"]), 1)
if __name__ == "__main__":
unittest.main()
+53 -1
View File
@@ -6,6 +6,7 @@ import unittest
import warnings
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__), "..", "src")))
@@ -26,7 +27,12 @@ class TestWinClientComponent(unittest.TestCase):
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
hashes=[HashAlgorithm.SHA256],
ciphers=[SymmetricKeyAlgorithm.AES256],
compression=[CompressionAlgorithm.Uncompressed]
compression=[
CompressionAlgorithm.ZIP,
CompressionAlgorithm.ZLIB,
CompressionAlgorithm.BZ2,
CompressionAlgorithm.Uncompressed
]
)
self.server_priv = key
self.server_pub = key.pubkey
@@ -184,6 +190,52 @@ class TestWinClientComponent(unittest.TestCase):
# Only rec 103 and 102 should be processed (101 is already sent, <= 100 breaks early)
self.assertEqual(logs, [103, 102])
def test_mtls_client_certificate_handling(self):
import shutil
test_dir = "test_win_mtls_certs"
os.makedirs(test_dir, exist_ok=True)
try:
from src import server_enrollment as se
ca_cert, ca_key, ca_pem, _ = se.generate_ca_if_needed(test_dir)
client_cert_pem, client_key_pem = se.issue_client_cert("win-client-test", ca_cert, ca_key)
with open(os.path.join(test_dir, "ca.crt"), "w") as f:
f.write(ca_pem)
with open(os.path.join(test_dir, "client.crt"), "w") as f:
f.write(client_cert_pem)
with open(os.path.join(test_dir, "client.key"), "w") as f:
f.write(client_key_pem)
# Test missing certs exception
empty_dir = "test_empty_certs"
os.makedirs(empty_dir, exist_ok=True)
with self.assertRaises(FileNotFoundError):
Win_Client.get_tls_socket("127.0.0.1", 9443, empty_dir)
shutil.rmtree(empty_dir, ignore_errors=True)
finally:
shutil.rmtree(test_dir, ignore_errors=True)
def test_client_certificate_validity_and_proactive_check(self):
import shutil
test_dir = "test_win_client_validity"
os.makedirs(test_dir, exist_ok=True)
try:
from src import server_enrollment as se
ca_cert, ca_key, _, _ = se.generate_ca_if_needed(test_dir)
client_cert_pem, client_key_pem = se.issue_client_cert("win-validity-test", ca_cert, ca_key, days_valid=365)
cert_path = os.path.join(test_dir, "client.crt")
with open(cert_path, "w", encoding="utf-8") as f:
f.write(client_cert_pem)
# Newly issued cert (365 days) is not expiring soon at 30 days
self.assertFalse(Win_Client.is_cert_expiring_soon(cert_path, threshold_days=30))
# Large threshold (500 days) reports expiring soon
self.assertTrue(Win_Client.is_cert_expiring_soon(cert_path, threshold_days=500))
# Non-existent file reports expiring / missing
self.assertTrue(Win_Client.is_cert_expiring_soon(os.path.join(test_dir, "missing.crt")))
finally:
shutil.rmtree(test_dir, ignore_errors=True)
if __name__ == "__main__":
unittest.main()