Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3234acf435 | ||
|
|
702ca985d2 | ||
|
|
97d92d1bee | ||
|
|
a8cd2cd83c | ||
|
|
6843b22ac1 | ||
|
|
bfd576bfdd | ||
|
|
9810220cb7 | ||
|
|
13ca1cacda | ||
|
|
4d607d0a9e | ||
|
|
ad80620e41 | ||
|
|
6fb631f908 | ||
|
|
65ec1b3b1b | ||
|
|
7b3b822974 | ||
|
|
5b97ccc1f2 | ||
|
|
d6e5ef7107 | ||
|
|
d689713234 | ||
|
|
61825571b1 | ||
|
|
06db6672ce | ||
|
|
a6eddc207c | ||
|
|
a70ce9162d |
@@ -9,6 +9,7 @@ __pycache__/
|
|||||||
*$py.class
|
*$py.class
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
out/
|
||||||
*.spec
|
*.spec
|
||||||
*.exe
|
*.exe
|
||||||
!compilation/nssm.exe
|
!compilation/nssm.exe
|
||||||
@@ -29,6 +30,7 @@ server_config.json
|
|||||||
client_config.json
|
client_config.json
|
||||||
test_*.json
|
test_*.json
|
||||||
test_*.db
|
test_*.db
|
||||||
|
master_ca_certs/
|
||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
runner/.env
|
runner/.env
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="https://gitea.eibl.tech/me0nline/LOGAR/raw/branch/main/assets/logo_banner.svg" alt="LOGAR Banner" width="750"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
# LOGAR: Fleet Log Aggregation & Centralized Provisioning System
|
# LOGAR: Fleet Log Aggregation & Centralized Provisioning System
|
||||||
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
|
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
|
||||||
|
|
||||||
[](RELEASE_NOTES.md)
|
[](RELEASE_NOTES.md)
|
||||||
[](tests/)
|
[](tests/)
|
||||||
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||||
|
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||||
|
|
||||||
**LOGAR** is an enterprise-grade log aggregation, normalization, and centralized provisioning system built for heterogeneous server fleets (Windows & Linux). It pairs ultra-lightweight, zero-state edge forwarders with a secure centralized server hub that enforces mutual TLS 1.3 (**mTLS**) authentication, dynamic PKI fleet licensing with cryptographic seat leasing, 12-hour temporal persistence tracking, and direct anomaly pass-through—delivering clean, structured, verified log telemetry directly to **Hermes AI** for autonomous deep reasoning, contextual root-cause analysis, and operational decision-making.
|
**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)**.
|
> 📚 **Comprehensive Technical Documentation**: Full architectural specifications, state machines, PKI X.509 OID definitions, branding assets, and complete REST API references are maintained on the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -107,8 +112,15 @@ Once installed or executed, access the web console:
|
|||||||
|
|
||||||
```
|
```
|
||||||
LOGAR/
|
LOGAR/
|
||||||
|
├── assets/ # Official vector SVGs, PNGs, ICOs, and branding bitmaps
|
||||||
|
│ ├── logo.svg # Master vector emblem
|
||||||
|
│ ├── logo_transparent.svg # Transparent WebUI emblem
|
||||||
|
│ ├── logo_banner.svg # Full brand banner
|
||||||
|
│ ├── logo.ico / favicon.ico # Application & web icons
|
||||||
|
│ └── wizard_*.bmp # Installer dialog bitmaps
|
||||||
├── .gitea/workflows/ # CI/CD pipelines (ci.yml, release.yml)
|
├── .gitea/workflows/ # CI/CD pipelines (ci.yml, release.yml)
|
||||||
├── compilation/ # Build scripts, Linux installers & Inno Setup scripts
|
├── compilation/ # Build scripts, Linux installers & Inno Setup scripts
|
||||||
|
│ ├── generate_branding_assets.py # Asset & bitmap generator
|
||||||
│ ├── install_linux_client.sh # Linux client systemd auto-installer
|
│ ├── install_linux_client.sh # Linux client systemd auto-installer
|
||||||
│ ├── install_linux_server.sh # Linux server systemd auto-installer
|
│ ├── install_linux_server.sh # Linux server systemd auto-installer
|
||||||
│ ├── package_dist.py # Standalone binary cross-compiler
|
│ ├── package_dist.py # Standalone binary cross-compiler
|
||||||
@@ -118,7 +130,7 @@ LOGAR/
|
|||||||
│ ├── server_enrollment.py # Sub-CA PKI and certificate generator
|
│ ├── server_enrollment.py # Sub-CA PKI and certificate generator
|
||||||
│ ├── Win_Client.py # Windows edge forwarder
|
│ ├── Win_Client.py # Windows edge forwarder
|
||||||
│ └── Linux_Client.py # Linux edge forwarder
|
│ └── Linux_Client.py # Linux edge forwarder
|
||||||
├── tests/ # Automated test suites (35 unit tests + pipeline test)
|
├── tests/ # Automated test suites (42 unit & integration tests)
|
||||||
├── LOGAR-LICENSING/ # Standalone Master License Authority (Git submodule)
|
├── LOGAR-LICENSING/ # Standalone Master License Authority (Git submodule)
|
||||||
├── RELEASE_NOTES.md # Version history and security changelog
|
├── RELEASE_NOTES.md # Version history and security changelog
|
||||||
└── README.md # Repository overview
|
└── README.md # Repository overview
|
||||||
@@ -146,7 +158,7 @@ LOGAR/
|
|||||||
## Testing & Cross-Compilation
|
## Testing & Cross-Compilation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run full unit test suite (35 tests)
|
# Run full unit test suite (42 tests)
|
||||||
python -m unittest discover -s tests -v
|
python -m unittest discover -s tests -v
|
||||||
|
|
||||||
# Run end-to-end mTLS pipeline integration test
|
# Run end-to-end mTLS pipeline integration test
|
||||||
@@ -162,6 +174,7 @@ python compilation/package_dist.py --target all
|
|||||||
|
|
||||||
For in-depth operational guides, lifecycle state machines, and PKI specifications, consult the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**:
|
For in-depth operational guides, lifecycle state machines, and PKI specifications, consult the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**:
|
||||||
|
|
||||||
|
* [Branding & Design System Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||||
* [Architecture & Design Details](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Architecture-and-Design)
|
* [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)
|
* [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)
|
* [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||||
|
|||||||
@@ -1,6 +1,37 @@
|
|||||||
# LOGAR Release v0.3.2
|
# LOGAR Release Notes
|
||||||
|
|
||||||
### 🪟 Windows Installer Fixes
|
## [v0.3.4] - 2026-09-27
|
||||||
|
|
||||||
|
### 🔄 In-Place Installer Upgrades & Mutual Exclusion Guards
|
||||||
|
- **Seamless In-Place Upgrades**: Windows (Inno Setup & NSIS) and Linux installers now detect existing installations automatically. In upgrade mode, installers stop background services, update core binaries and runtime in-place, and restart services without overwriting configuration files (`server_config.json`, `client_config.json`), database state (`logar_state.db`), state tracking (`client_state.json`), or mTLS certificates.
|
||||||
|
- **Host Mutual Exclusion Validation**: Added pre-installation validation across all installers to prevent accidental co-installation of LOGAR Server and LOGAR Client on the same host, showing descriptive guidance to administrators.
|
||||||
|
|
||||||
|
### 🛡️ WebUI 2FA & Telemetry Stability
|
||||||
|
- **Server Hub TOTP Two-Factor Authentication**: Implemented RFC 6238 TOTP 2FA for the LOGAR Server Hub web portal (`:8443`), featuring interactive QR code setup, secret generation, and secure session validation.
|
||||||
|
- **Telemetry Stream UI Buffer Capping**: Limited the live verified telemetry stream view to the latest 50 events in the Server Hub dashboard for enhanced browser performance and UI stability.
|
||||||
|
|
||||||
|
### 🔑 Licensing Authority Management & Registry Tooling
|
||||||
|
- **Revocation, Reinstatement & License Deletion**: Enhanced the Master Licensing Authority (`logar.eibl.tech`) with instant license revocation, reinstatement of revoked licenses, and conditional permanent deletion of revoked records.
|
||||||
|
- **Interactive Capability Explorer**: Added interactive implementation breakdown modals across core capabilities on the public project landing page.
|
||||||
|
- **Automated Gitea Container Pruning**: Added `prune_images.py` utility to maintain clean container registries by keeping only the latest active image versions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [v0.3.3] - 2026-09-27
|
||||||
|
|
||||||
|
### 🎨 Brand Identity, Centralized Asset Library & Design System
|
||||||
|
- **Centralized `/assets` Repository**: Added official vector graphics (`logo.svg`, `logo_transparent.svg`, `logo_dark.svg`, `logo_banner.svg`), high-resolution master PNGs (16px to 512px), multi-resolution Windows ICOs (`logo.ico`, `favicon.ico`), and installer dialog bitmaps (`wizard_header.bmp`, `wizard_small.bmp`, `wizard_sidebar.bmp`).
|
||||||
|
- **Official Wiki Branding Guide**: Published comprehensive visual identity standards on the Gitea Wiki at `https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide` detailing logo anatomy, color palettes (Cyber Cyan `#00E5FF`, Emerald `#00FFA3`, Dark Navy `#0E1A2E`), typography, clear-space rules, dos and don'ts, and strict non-SIEM positioning.
|
||||||
|
- **Unified Interface & Tab Branding**: Implemented transparent vector logos on all web navigation bars and browser tabs across both the LOGAR Server Hub (`:8443`) and Master Licensing Authority (`logar.eibl.tech`).
|
||||||
|
- **Clean Installer Bitmaps**: Regenerated 24-bit RGB header bitmaps with seamless white backgrounds (`#FFFFFF`) matching native Windows Inno Setup wizard dialogs.
|
||||||
|
- **Automated Asset Pipeline**: Enhanced `compilation/generate_branding_assets.py` to deterministically compile and distribute assets across main repo, compilation subdirectories, and the licensing submodule without third-party graphics dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [v0.3.2] - 2026-09-27
|
||||||
|
|
||||||
|
### 🪟 Windows Installer & Web Portal Enhancements
|
||||||
|
- **Project Showcase Landing Page**: Deployed high-impact public homepage at `GET /` on the licensing authority with architecture diagrams, quickstart commands, and direct navigation to Gitea, Wiki, and the Licensing Portal (`/licensing`).
|
||||||
- **Standalone Windows Binaries & Python Runtime**: Resolved issues with standalone Windows executable execution and bundled runtime paths.
|
- **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.
|
- **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.
|
- **Service Lifecycle & Clean Uninstallation**: Resolved service startup issues and ensured complete cleanup of state files upon uninstallation.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -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 |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 650 B |
|
After Width: | Height: | Size: 921 B |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,54 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 240" width="100%" height="100%">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bannerBg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0E1A2E"/>
|
||||||
|
<stop offset="100%" stop-color="#070C18"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="textCyan" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#00E5FF"/>
|
||||||
|
<stop offset="100%" stop-color="#00FFA3"/>
|
||||||
|
</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="bannerGlow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background Card -->
|
||||||
|
<rect width="960" height="240" rx="16" fill="url(#bannerBg)" stroke="#162A45" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Left Emblem Group (scaled & positioned) -->
|
||||||
|
<g transform="translate(40, 20) scale(0.39)">
|
||||||
|
<!-- Hexagon Shield -->
|
||||||
|
<polygon points="256,24 456,139 456,373 256,488 56,373 56,139"
|
||||||
|
fill="#0E1A2E" stroke="url(#cyanGrad)" stroke-width="12" filter="url(#bannerGlow)"/>
|
||||||
|
<!-- Inner Telemetry Ring -->
|
||||||
|
<polygon points="256,58 426,156 426,356 256,454 86,356 86,156"
|
||||||
|
fill="none" stroke="#162A45" stroke-width="4" stroke-dasharray="10 8"/>
|
||||||
|
<!-- Stylized L & Telemetry Bars -->
|
||||||
|
<rect x="140" y="140" width="46" height="232" rx="10" fill="url(#cyanGrad)"/>
|
||||||
|
<rect x="140" y="326" width="220" height="46" rx="10" fill="url(#cyanGrad)"/>
|
||||||
|
<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 Points -->
|
||||||
|
<circle cx="238" cy="190" r="9" fill="#00FFA3"/>
|
||||||
|
<circle cx="304" cy="130" r="9" fill="#00E5FF"/>
|
||||||
|
<circle cx="380" cy="349" r="10" fill="#00E5FF"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Typography & Tagline -->
|
||||||
|
<text x="270" y="98" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="56" font-weight="900" letter-spacing="4" fill="url(#textCyan)" filter="url(#bannerGlow)">LOGAR</text>
|
||||||
|
<text x="272" y="142" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="20" font-weight="600" fill="#E2E8F0" letter-spacing="0.5">Fleet Log Aggregation & Telemetry Hub</text>
|
||||||
|
<text x="272" y="174" font-family="'JetBrains Mono', 'Fira Code', monospace" font-size="13" font-weight="400" fill="#00E5FF" letter-spacing="1">ZERO-STATE FORWARDERS • MUTUAL TLS 1.3 • TEMPORAL PERSISTENCE</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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 |
@@ -0,0 +1,41 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 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: 1.8 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#0072FF" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#00E5FF" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#00FFA3" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#070C18" stroke="#1e293b" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#0E1A2E" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#94A3B8" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#EF4444" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#F59E0B" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#FFFFFF" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 351 B |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 650 B |
|
After Width: | Height: | Size: 921 B |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,54 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 240" width="100%" height="100%">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bannerBg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0E1A2E"/>
|
||||||
|
<stop offset="100%" stop-color="#070C18"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="textCyan" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#00E5FF"/>
|
||||||
|
<stop offset="100%" stop-color="#00FFA3"/>
|
||||||
|
</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="bannerGlow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background Card -->
|
||||||
|
<rect width="960" height="240" rx="16" fill="url(#bannerBg)" stroke="#162A45" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Left Emblem Group (scaled & positioned) -->
|
||||||
|
<g transform="translate(40, 20) scale(0.39)">
|
||||||
|
<!-- Hexagon Shield -->
|
||||||
|
<polygon points="256,24 456,139 456,373 256,488 56,373 56,139"
|
||||||
|
fill="#0E1A2E" stroke="url(#cyanGrad)" stroke-width="12" filter="url(#bannerGlow)"/>
|
||||||
|
<!-- Inner Telemetry Ring -->
|
||||||
|
<polygon points="256,58 426,156 426,356 256,454 86,356 86,156"
|
||||||
|
fill="none" stroke="#162A45" stroke-width="4" stroke-dasharray="10 8"/>
|
||||||
|
<!-- Stylized L & Telemetry Bars -->
|
||||||
|
<rect x="140" y="140" width="46" height="232" rx="10" fill="url(#cyanGrad)"/>
|
||||||
|
<rect x="140" y="326" width="220" height="46" rx="10" fill="url(#cyanGrad)"/>
|
||||||
|
<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 Points -->
|
||||||
|
<circle cx="238" cy="190" r="9" fill="#00FFA3"/>
|
||||||
|
<circle cx="304" cy="130" r="9" fill="#00E5FF"/>
|
||||||
|
<circle cx="380" cy="349" r="10" fill="#00E5FF"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Typography & Tagline -->
|
||||||
|
<text x="270" y="98" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="56" font-weight="900" letter-spacing="4" fill="url(#textCyan)" filter="url(#bannerGlow)">LOGAR</text>
|
||||||
|
<text x="272" y="142" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="20" font-weight="600" fill="#E2E8F0" letter-spacing="0.5">Fleet Log Aggregation & Telemetry Hub</text>
|
||||||
|
<text x="272" y="174" font-family="'JetBrains Mono', 'Fira Code', monospace" font-size="13" font-weight="400" fill="#00E5FF" letter-spacing="1">ZERO-STATE FORWARDERS • MUTUAL TLS 1.3 • TEMPORAL PERSISTENCE</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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 |
@@ -0,0 +1,41 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 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: 1.8 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#0072FF" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#00E5FF" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#00FFA3" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#070C18" stroke="#1e293b" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#0E1A2E" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#94A3B8" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#EF4444" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#F59E0B" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="#FFFFFF" stroke="#334155" stroke-width="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
LOGAR Branding & Installer Graphics Generator
|
LOGAR Branding & Installer Graphics Generator
|
||||||
Generates clean, professional SVG, PNG, ICO, and BMP visual assets for
|
Generates clean, professional SVG, PNG, ICO, and BMP visual assets for
|
||||||
LOGAR installers (Inno Setup & NSIS), application icons, and WebUI branding.
|
LOGAR installers (Inno Setup & NSIS), application icons, root /assets library, and WebUI branding.
|
||||||
Zero external dependencies required (uses standard library zlib and struct).
|
Zero external dependencies required (uses standard library zlib and struct).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -9,9 +9,15 @@ import os
|
|||||||
import math
|
import math
|
||||||
import zlib
|
import zlib
|
||||||
import struct
|
import struct
|
||||||
|
import shutil
|
||||||
|
|
||||||
ASSETS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
os.makedirs(ASSETS_DIR, exist_ok=True)
|
ROOT_ASSETS_DIR = os.path.join(ROOT_DIR, "assets")
|
||||||
|
COMPILATION_ASSETS_DIR = os.path.join(ROOT_DIR, "compilation", "assets")
|
||||||
|
LICENSING_ASSETS_DIR = os.path.join(ROOT_DIR, "LOGAR-LICENSING", "assets")
|
||||||
|
|
||||||
|
for d in (ROOT_ASSETS_DIR, COMPILATION_ASSETS_DIR, LICENSING_ASSETS_DIR):
|
||||||
|
os.makedirs(d, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def create_bmp_24bit(width: int, height: int, pixel_func) -> bytes:
|
def create_bmp_24bit(width: int, height: int, pixel_func) -> bytes:
|
||||||
@@ -80,7 +86,7 @@ def create_png_rgba(width: int, height: int, pixel_func) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def draw_logo_rgba(x: int, y: int, w: int, h: int) -> tuple:
|
def draw_logo_rgba(x: int, y: int, w: int, h: int) -> tuple:
|
||||||
"""Renders a sleek modern cyberpunk hexagon shield and pulse log streams."""
|
"""Renders a sleek modern hexagon shield and pulse log streams."""
|
||||||
nx = (x - w / 2) / (w / 2)
|
nx = (x - w / 2) / (w / 2)
|
||||||
ny = (y - h / 2) / (h / 2)
|
ny = (y - h / 2) / (h / 2)
|
||||||
dist = math.sqrt(nx * nx + ny * ny)
|
dist = math.sqrt(nx * nx + ny * ny)
|
||||||
@@ -144,12 +150,13 @@ 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)
|
Renders header BMP graphic with a pure white background (255, 255, 255)
|
||||||
matching the installer wizard dialog background seamlessly.
|
matching the installer wizard dialog background seamlessly.
|
||||||
The logo is positioned on the left with clean alpha-blended edges.
|
The logo is positioned on the right (icon_center_x = w - 32, center y = h / 2)
|
||||||
|
allowing the header title and subtext to align cleanly to the far left.
|
||||||
"""
|
"""
|
||||||
bg_r, bg_g, bg_b = 255, 255, 255
|
bg_r, bg_g, bg_b = 255, 255, 255
|
||||||
|
|
||||||
# Position emblem cleanly on the left (center x = 30, center y = h / 2)
|
# Position emblem cleanly on the right
|
||||||
icon_center_x = 28
|
icon_center_x = w - 32
|
||||||
icon_center_y = h / 2
|
icon_center_y = h / 2
|
||||||
icon_radius = 22
|
icon_radius = 22
|
||||||
dx = (x - icon_center_x) / icon_radius
|
dx = (x - icon_center_x) / icon_radius
|
||||||
@@ -192,7 +199,7 @@ def draw_wizard_sidebar_bgr(x: int, y: int, w: int, h: int) -> tuple:
|
|||||||
"""Renders 164x314 BMP sidebar graphic for NSIS / Inno Setup."""
|
"""Renders 164x314 BMP sidebar graphic for NSIS / Inno Setup."""
|
||||||
t_y = y / h
|
t_y = y / h
|
||||||
|
|
||||||
# Deep cyber navy to midnight blue vertical gradient
|
# Deep navy to midnight blue vertical gradient
|
||||||
r = int(10 * (1 - t_y) + 4 * t_y)
|
r = int(10 * (1 - t_y) + 4 * t_y)
|
||||||
g = int(22 * (1 - t_y) + 12 * t_y)
|
g = int(22 * (1 - t_y) + 12 * t_y)
|
||||||
b = int(48 * (1 - t_y) + 26 * t_y)
|
b = int(48 * (1 - t_y) + 26 * t_y)
|
||||||
@@ -262,9 +269,13 @@ def generate_ico_file(png_images: list) -> bytes:
|
|||||||
return header + bytes(entries) + b"".join(image_data_blocks)
|
return header + bytes(entries) + b"".join(image_data_blocks)
|
||||||
|
|
||||||
|
|
||||||
def generate_svg_logo() -> str:
|
def generate_svg_logo(transparent_bg: bool = False) -> str:
|
||||||
"""Generates a high-quality SVG vector graphic for LOGAR."""
|
"""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%">
|
bg_polygon = "" if transparent_bg else """ <!-- 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)"/>"""
|
||||||
|
|
||||||
|
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
<stop offset="0%" stop-color="#0E1A2E"/>
|
<stop offset="0%" stop-color="#0E1A2E"/>
|
||||||
@@ -288,9 +299,7 @@ def generate_svg_logo() -> str:
|
|||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- Base Hexagon Shield -->
|
{bg_polygon}
|
||||||
<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 -->
|
<!-- Inner Telemetry Grid / Ring -->
|
||||||
<polygon points="256,58 426,156 426,356 256,454 86,356 86,156"
|
<polygon points="256,58 426,156 426,356 256,454 86,356 86,156"
|
||||||
@@ -309,56 +318,150 @@ def generate_svg_logo() -> str:
|
|||||||
</svg>"""
|
</svg>"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_svg_banner() -> str:
|
||||||
|
"""Generates a wide SVG banner with emblem and modern typography."""
|
||||||
|
return """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 240" width="100%" height="100%">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bannerBg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0E1A2E"/>
|
||||||
|
<stop offset="100%" stop-color="#070C18"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="textCyan" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#00E5FF"/>
|
||||||
|
<stop offset="100%" stop-color="#00FFA3"/>
|
||||||
|
</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="bannerGlow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background Card -->
|
||||||
|
<rect width="960" height="240" rx="16" fill="url(#bannerBg)" stroke="#162A45" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Left Emblem Group (scaled & positioned) -->
|
||||||
|
<g transform="translate(40, 20) scale(0.39)">
|
||||||
|
<!-- Hexagon Shield -->
|
||||||
|
<polygon points="256,24 456,139 456,373 256,488 56,373 56,139"
|
||||||
|
fill="#0E1A2E" stroke="url(#cyanGrad)" stroke-width="12" filter="url(#bannerGlow)"/>
|
||||||
|
<!-- Inner Telemetry Ring -->
|
||||||
|
<polygon points="256,58 426,156 426,356 256,454 86,356 86,156"
|
||||||
|
fill="none" stroke="#162A45" stroke-width="4" stroke-dasharray="10 8"/>
|
||||||
|
<!-- Stylized L & Telemetry Bars -->
|
||||||
|
<rect x="140" y="140" width="46" height="232" rx="10" fill="url(#cyanGrad)"/>
|
||||||
|
<rect x="140" y="326" width="220" height="46" rx="10" fill="url(#cyanGrad)"/>
|
||||||
|
<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 Points -->
|
||||||
|
<circle cx="238" cy="190" r="9" fill="#00FFA3"/>
|
||||||
|
<circle cx="304" cy="130" r="9" fill="#00E5FF"/>
|
||||||
|
<circle cx="380" cy="349" r="10" fill="#00E5FF"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Typography & Tagline -->
|
||||||
|
<text x="270" y="98" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="56" font-weight="900" letter-spacing="4" fill="url(#textCyan)" filter="url(#bannerGlow)">LOGAR</text>
|
||||||
|
<text x="272" y="142" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="20" font-weight="600" fill="#E2E8F0" letter-spacing="0.5">Fleet Log Aggregation & Telemetry Hub</text>
|
||||||
|
<text x="272" y="174" font-family="'JetBrains Mono', 'Fira Code', monospace" font-size="13" font-weight="400" fill="#00E5FF" letter-spacing="1">ZERO-STATE FORWARDERS • MUTUAL TLS 1.3 • TEMPORAL PERSISTENCE</text>
|
||||||
|
</svg>"""
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("[*] Generating high-resolution branding assets for LOGAR...")
|
print("[*] Generating comprehensive branding assets for LOGAR...")
|
||||||
|
|
||||||
# 1. Generate SVG Logo
|
# 1. Generate SVG Logos
|
||||||
svg_path = os.path.join(ASSETS_DIR, "logo.svg")
|
svg_logo_dark = generate_svg_logo(transparent_bg=False)
|
||||||
with open(svg_path, "w", encoding="utf-8") as f:
|
svg_logo_trans = generate_svg_logo(transparent_bg=True)
|
||||||
f.write(generate_svg_logo())
|
svg_banner = generate_svg_banner()
|
||||||
print(f"[+] Saved SVG logo: {svg_path}")
|
|
||||||
|
|
||||||
# 2. Generate PNG images of various sizes
|
# 2. Generate PNG images across multiple dimensions
|
||||||
png_sizes = [16, 32, 48, 64, 128, 256, 512]
|
png_sizes = [16, 32, 48, 64, 128, 256, 512]
|
||||||
png_buffers = []
|
png_buffers = []
|
||||||
|
png_dict = {}
|
||||||
for s in png_sizes:
|
for s in png_sizes:
|
||||||
png_data = create_png_rgba(s, s, draw_logo_rgba)
|
png_data = create_png_rgba(s, s, draw_logo_rgba)
|
||||||
png_buffers.append((s, s, png_data))
|
png_buffers.append((s, s, png_data))
|
||||||
if s in [256, 512]:
|
png_dict[s] = png_data
|
||||||
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
|
# 3. Generate Windows ICO File (multi-res 16..256)
|
||||||
ico_path = os.path.join(ASSETS_DIR, "logo.ico")
|
ico_data = generate_ico_file(png_buffers[:-1])
|
||||||
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
|
# 4. Generate BMP files for installers
|
||||||
header_bmp_path = os.path.join(ASSETS_DIR, "wizard_header.bmp")
|
|
||||||
header_bmp_data = create_bmp_24bit(150, 57, draw_wizard_header_white_bgr)
|
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)
|
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)
|
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.")
|
# 5. Generate Palette Swatch SVGs
|
||||||
|
swatches = {
|
||||||
|
"00e5ff": "#00E5FF",
|
||||||
|
"00ffa3": "#00FFA3",
|
||||||
|
"0072ff": "#0072FF",
|
||||||
|
"0e1a2e": "#0E1A2E",
|
||||||
|
"070c18": "#070C18",
|
||||||
|
"ef4444": "#EF4444",
|
||||||
|
"f59e0b": "#F59E0B",
|
||||||
|
"94a3b8": "#94A3B8",
|
||||||
|
"ffffff": "#FFFFFF",
|
||||||
|
}
|
||||||
|
swatch_svgs = {}
|
||||||
|
for name, hex_code in swatches.items():
|
||||||
|
border = "#334155" if hex_code.upper() != "#070C18" else "#1e293b"
|
||||||
|
swatch_svgs[name] = f'<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="16" height="16" rx="4" fill="{hex_code}" stroke="{border}" stroke-width="1.5"/></svg>'
|
||||||
|
|
||||||
|
# 6. Write to target directories
|
||||||
|
for target_dir in (ROOT_ASSETS_DIR, COMPILATION_ASSETS_DIR, LICENSING_ASSETS_DIR):
|
||||||
|
swatch_dir = os.path.join(target_dir, "swatches")
|
||||||
|
os.makedirs(swatch_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Swatches
|
||||||
|
for name, svg_content in swatch_svgs.items():
|
||||||
|
with open(os.path.join(swatch_dir, f"swatch_{name}.svg"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(svg_content)
|
||||||
|
|
||||||
|
# SVGs
|
||||||
|
with open(os.path.join(target_dir, "logo.svg"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(svg_logo_dark)
|
||||||
|
with open(os.path.join(target_dir, "logo_dark.svg"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(svg_logo_dark)
|
||||||
|
with open(os.path.join(target_dir, "logo_transparent.svg"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(svg_logo_trans)
|
||||||
|
with open(os.path.join(target_dir, "logo_banner.svg"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(svg_banner)
|
||||||
|
|
||||||
|
# PNGs
|
||||||
|
with open(os.path.join(target_dir, "logo.png"), "wb") as f:
|
||||||
|
f.write(png_dict[512])
|
||||||
|
for s in png_sizes:
|
||||||
|
with open(os.path.join(target_dir, f"logo_{s}x{s}.png"), "wb") as f:
|
||||||
|
f.write(png_dict[s])
|
||||||
|
|
||||||
|
# ICOs
|
||||||
|
with open(os.path.join(target_dir, "logo.ico"), "wb") as f:
|
||||||
|
f.write(ico_data)
|
||||||
|
with open(os.path.join(target_dir, "favicon.ico"), "wb") as f:
|
||||||
|
f.write(ico_data)
|
||||||
|
|
||||||
|
# BMPs
|
||||||
|
with open(os.path.join(target_dir, "wizard_header.bmp"), "wb") as f:
|
||||||
|
f.write(header_bmp_data)
|
||||||
|
with open(os.path.join(target_dir, "wizard_small.bmp"), "wb") as f:
|
||||||
|
f.write(small_bmp_data)
|
||||||
|
with open(os.path.join(target_dir, "wizard_sidebar.bmp"), "wb") as f:
|
||||||
|
f.write(sidebar_bmp_data)
|
||||||
|
|
||||||
|
print(f"[+] Populated asset repository: {target_dir}")
|
||||||
|
|
||||||
|
print("[+] All LOGAR branding assets, icons, and banners generated successfully!")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -7,6 +7,26 @@ ENROLL_SECRET="${2:-}"
|
|||||||
INSTALL_DIR="/opt/logar-client"
|
INSTALL_DIR="/opt/logar-client"
|
||||||
CONFIG_DIR="/etc/logar"
|
CONFIG_DIR="/etc/logar"
|
||||||
|
|
||||||
|
# 1. Mutual Exclusion Check: Deny installation if LOGAR Server is present
|
||||||
|
if [ -f "/opt/logar-server/Server" ] || [ -f "/etc/systemd/system/logar-server.service" ]; then
|
||||||
|
echo "[-] Error: LOGAR Server is already installed on this machine." >&2
|
||||||
|
echo "[-] LOGAR Client cannot be installed on a host where LOGAR Server is present (LOGAR Server includes built-in log collection)." >&2
|
||||||
|
echo "[-] Please uninstall LOGAR Server first before installing LOGAR Client." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Existing LOGAR Client Detection for In-Place Update Mode
|
||||||
|
IS_UPDATE=0
|
||||||
|
if [ -f "${INSTALL_DIR}/Linux_Client" ] || [ -f "/etc/systemd/system/logar-client.service" ]; then
|
||||||
|
IS_UPDATE=1
|
||||||
|
echo "[*] Existing LOGAR Client installation detected. Performing in-place upgrade..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${IS_UPDATE}" -eq 1 ] && command -v systemctl >/dev/null 2>&1; then
|
||||||
|
echo "[*] Stopping LOGAR Client service for upgrade..."
|
||||||
|
systemctl stop logar-client.service || true
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[+] Installing LOGAR Client..."
|
echo "[+] Installing LOGAR Client..."
|
||||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
|
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
|
||||||
chmod 700 "${CONFIG_DIR}/certs"
|
chmod 700 "${CONFIG_DIR}/certs"
|
||||||
|
|||||||
@@ -21,14 +21,34 @@ echo "============================================================"
|
|||||||
echo " LOGAR Server Enterprise Linux Installer"
|
echo " LOGAR Server Enterprise Linux Installer"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
# Interactive configuration if running in a terminal without CLI arguments
|
# 1. Mutual Exclusion Check: Deny installation if LOGAR Client is present
|
||||||
if [ -t 0 ]; then
|
if [ -f "/opt/logar-client/Linux_Client" ] || [ -f "/etc/systemd/system/logar-client.service" ]; then
|
||||||
|
echo "[-] Error: LOGAR Client is already installed on this machine." >&2
|
||||||
|
echo "[-] LOGAR Server cannot be installed on a host where LOGAR Client is present (LOGAR Server includes built-in log collection)." >&2
|
||||||
|
echo "[-] Please uninstall LOGAR Client first before installing LOGAR Server." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Existing LOGAR Server Detection for In-Place Update Mode
|
||||||
|
IS_UPDATE=0
|
||||||
|
if [ -f "${INSTALL_DIR}/Server" ] || [ -f "/etc/systemd/system/logar-server.service" ]; then
|
||||||
|
IS_UPDATE=1
|
||||||
|
echo "[*] Existing LOGAR Server installation detected. Performing in-place upgrade..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Interactive configuration if running in a terminal without CLI arguments (only for fresh install)
|
||||||
|
if [ "${IS_UPDATE}" -eq 0 ] && [ -t 0 ]; then
|
||||||
read -r -p "Enter Data and Configuration Directory [${CONFIG_DIR}]: " USER_CFG
|
read -r -p "Enter Data and Configuration Directory [${CONFIG_DIR}]: " USER_CFG
|
||||||
if [ -n "${USER_CFG}" ]; then
|
if [ -n "${USER_CFG}" ]; then
|
||||||
CONFIG_DIR="${USER_CFG}"
|
CONFIG_DIR="${USER_CFG}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "${IS_UPDATE}" -eq 1 ] && command -v systemctl >/dev/null 2>&1; then
|
||||||
|
echo "[*] Stopping LOGAR Server service for upgrade..."
|
||||||
|
systemctl stop logar-server.service || true
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
|
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
|
||||||
|
|
||||||
if [ -f "dist/Server.bin" ]; then
|
if [ -f "dist/Server.bin" ]; then
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
[Setup]
|
[Setup]
|
||||||
|
AppId={{9C4A6F1D-8975-476E-8512-4217DE029A88}}
|
||||||
AppName=LOGAR Client
|
AppName=LOGAR Client
|
||||||
AppVersion=0.3.2
|
AppVersion=0.3.4
|
||||||
AppPublisher=LOGAR Security Systems
|
AppPublisher=LOGAR
|
||||||
AppPublisherURL=https://logar.eibl.tech
|
AppPublisherURL=https://logar.eibl.tech
|
||||||
AppSupportURL=https://logar.eibl.tech
|
AppSupportURL=https://logar.eibl.tech
|
||||||
AppUpdatesURL=https://logar.eibl.tech
|
AppUpdatesURL=https://logar.eibl.tech
|
||||||
DefaultDirName={autopf}\LOGAR
|
DefaultDirName={autopf}\LOGAR
|
||||||
|
UsePreviousAppDir=yes
|
||||||
OutputDir=..\dist
|
OutputDir=..\dist
|
||||||
OutputBaseFilename=LOGAR-Client-Setup
|
OutputBaseFilename=LOGAR-Client-Setup
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=admin
|
||||||
@@ -15,11 +17,11 @@ WizardStyle=modern
|
|||||||
SetupIconFile=assets\logo.ico
|
SetupIconFile=assets\logo.ico
|
||||||
WizardImageFile=assets\wizard_sidebar.bmp
|
WizardImageFile=assets\wizard_sidebar.bmp
|
||||||
WizardSmallImageFile=assets\wizard_small.bmp
|
WizardSmallImageFile=assets\wizard_small.bmp
|
||||||
VersionInfoCompany=LOGAR Security Systems
|
VersionInfoCompany=LOGAR
|
||||||
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
|
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
|
||||||
VersionInfoVersion=0.3.2.0
|
VersionInfoVersion=0.3.4.0
|
||||||
VersionInfoProductName=LOGAR Client
|
VersionInfoProductName=LOGAR Client
|
||||||
VersionInfoProductVersion=0.3.2
|
VersionInfoProductVersion=0.3.4
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
@@ -54,6 +56,58 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR"
|
|||||||
var
|
var
|
||||||
DataDirPage: TInputDirWizardPage;
|
DataDirPage: TInputDirWizardPage;
|
||||||
ConfigFilePage: TInputFileWizardPage;
|
ConfigFilePage: TInputFileWizardPage;
|
||||||
|
IsUpdateMode: Boolean;
|
||||||
|
ExistingDataDir: String;
|
||||||
|
|
||||||
|
function InitializeSetup(): Boolean;
|
||||||
|
var
|
||||||
|
ServerFound: Boolean;
|
||||||
|
Val: String;
|
||||||
|
begin
|
||||||
|
Result := True;
|
||||||
|
IsUpdateMode := False;
|
||||||
|
ExistingDataDir := '';
|
||||||
|
ServerFound := False;
|
||||||
|
|
||||||
|
// 1. Mutual Exclusion Check: Deny installation if LOGAR Server is already present
|
||||||
|
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server') or
|
||||||
|
RegValueExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
|
||||||
|
FileExists(ExpandConstant('{autopf}\LOGAR-Server\Server.exe')) then
|
||||||
|
begin
|
||||||
|
ServerFound := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if ServerFound then
|
||||||
|
begin
|
||||||
|
MsgBox('LOGAR Server is already installed on this machine.'#13#10#13#10 +
|
||||||
|
'LOGAR Client cannot be installed on a LOGAR Server host (LOGAR Server already provides built-in log collection).'#13#10#13#10 +
|
||||||
|
'Please uninstall LOGAR Server first if you wish to run a standalone forwarder client.',
|
||||||
|
mbCriticalError, MB_OK);
|
||||||
|
Result := False;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 2. Existing LOGAR Client Detection for In-Place Update Mode
|
||||||
|
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
|
||||||
|
FileExists(ExpandConstant('{autopf}\LOGAR\Win_Client.exe')) then
|
||||||
|
begin
|
||||||
|
IsUpdateMode := True;
|
||||||
|
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', Val) then
|
||||||
|
ExistingDataDir := Val;
|
||||||
|
|
||||||
|
MsgBox('An existing installation of LOGAR Client was detected.'#13#10#13#10 +
|
||||||
|
'Setup will perform an in-place update: core binaries will be updated, while your existing configuration (client_config.json), state tracking, and certificates will be preserved.',
|
||||||
|
mbInformation, MB_OK);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure InitializeWizard;
|
procedure InitializeWizard;
|
||||||
begin
|
begin
|
||||||
@@ -67,7 +121,10 @@ begin
|
|||||||
''
|
''
|
||||||
);
|
);
|
||||||
DataDirPage.Add('Data Directory:');
|
DataDirPage.Add('Data Directory:');
|
||||||
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
|
if (ExistingDataDir <> '') then
|
||||||
|
DataDirPage.Values[0] := ExistingDataDir
|
||||||
|
else
|
||||||
|
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
|
||||||
|
|
||||||
ConfigFilePage := CreateInputFilePage(
|
ConfigFilePage := CreateInputFilePage(
|
||||||
DataDirPage.ID,
|
DataDirPage.ID,
|
||||||
@@ -80,8 +137,23 @@ begin
|
|||||||
ConfigFilePage.Add('Client Configuration (client_config.json):', 'JSON Files (*.json)|*.json|All Files (*.*)|*.*', '.json');
|
ConfigFilePage.Add('Client Configuration (client_config.json):', 'JSON Files (*.json)|*.json|All Files (*.*)|*.*', '.json');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if IsUpdateMode then
|
||||||
|
begin
|
||||||
|
if (PageID = wpSelectDir) or (PageID = DataDirPage.ID) or (PageID = ConfigFilePage.ID) then
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function GetDataDir(Param: String): String;
|
function GetDataDir(Param: String): String;
|
||||||
begin
|
begin
|
||||||
|
if IsUpdateMode and (ExistingDataDir <> '') then
|
||||||
|
begin
|
||||||
|
Result := ExistingDataDir;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
Result := DataDirPage.Values[0];
|
Result := DataDirPage.Values[0];
|
||||||
if Result = '' then
|
if Result = '' then
|
||||||
Result := ExpandConstant('{commonappdata}\LOGAR');
|
Result := ExpandConstant('{commonappdata}\LOGAR');
|
||||||
@@ -107,13 +179,17 @@ begin
|
|||||||
end
|
end
|
||||||
else if CurStep = ssPostInstall then
|
else if CurStep = ssPostInstall then
|
||||||
begin
|
begin
|
||||||
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'InstallDir', ExpandConstant('{app}'));
|
||||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
|
||||||
|
|
||||||
CfgPath := GetConfigFile('');
|
if not IsUpdateMode then
|
||||||
if (CfgPath <> '') and FileExists(CfgPath) then
|
|
||||||
begin
|
begin
|
||||||
DestCfg := GetDataDir('') + '\client_config.json';
|
CfgPath := GetConfigFile('');
|
||||||
FileCopy(CfgPath, DestCfg, False);
|
if (CfgPath <> '') and FileExists(CfgPath) then
|
||||||
|
begin
|
||||||
|
DestCfg := GetDataDir('') + '\client_config.json';
|
||||||
|
FileCopy(CfgPath, DestCfg, False);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -9,24 +9,26 @@ RequestExecutionLevel admin
|
|||||||
|
|
||||||
BrandingText "LOGAR Centralized Log Aggregator & Forwarder"
|
BrandingText "LOGAR Centralized Log Aggregator & Forwarder"
|
||||||
|
|
||||||
VIProductVersion "0.3.2.0"
|
VIProductVersion "0.3.4.0"
|
||||||
VIAddVersionKey "ProductName" "LOGAR Client"
|
VIAddVersionKey "ProductName" "LOGAR Client"
|
||||||
VIAddVersionKey "CompanyName" "LOGAR Security Systems"
|
VIAddVersionKey "CompanyName" "LOGAR"
|
||||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR Security Systems"
|
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
||||||
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
|
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
|
||||||
VIAddVersionKey "FileVersion" "0.3.2.0"
|
VIAddVersionKey "FileVersion" "0.3.4.0"
|
||||||
VIAddVersionKey "ProductVersion" "0.3.2.0"
|
VIAddVersionKey "ProductVersion" "0.3.4.0"
|
||||||
|
|
||||||
# --- Visual Graphics & Branding ---
|
# --- Visual Graphics & Branding ---
|
||||||
!define MUI_ICON "assets/logo.ico"
|
!define MUI_ICON "assets/logo.ico"
|
||||||
!define MUI_UNICON "assets/logo.ico"
|
!define MUI_UNICON "assets/logo.ico"
|
||||||
!define MUI_HEADERIMAGE
|
!define MUI_HEADERIMAGE
|
||||||
|
!define MUI_HEADERIMAGE_RIGHT
|
||||||
!define MUI_HEADERIMAGE_BITMAP "assets/wizard_header.bmp"
|
!define MUI_HEADERIMAGE_BITMAP "assets/wizard_header.bmp"
|
||||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
||||||
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
||||||
|
|
||||||
Var DATADIR
|
Var DATADIR
|
||||||
Var CFGFILE
|
Var CFGFILE
|
||||||
|
Var IS_UPDATE
|
||||||
Var CfgDialog
|
Var CfgDialog
|
||||||
Var CfgFileInput
|
Var CfgFileInput
|
||||||
Var CfgBrowseBtn
|
Var CfgBrowseBtn
|
||||||
@@ -42,6 +44,7 @@ Var CfgBrowseBtn
|
|||||||
# Directory Page 1: Installation Directory
|
# Directory Page 1: Installation Directory
|
||||||
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
||||||
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||||
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
@@ -50,6 +53,7 @@ Var CfgBrowseBtn
|
|||||||
!define MUI_PAGE_HEADER_SUBTEXT "Choose where state tracking, certificates, and configuration files should be stored."
|
!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_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_TEXT_DESTINATION "Data & Storage Folder"
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||||
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
@@ -70,13 +74,51 @@ Page custom ConfigPageCreate ConfigPageLeave
|
|||||||
|
|
||||||
!insertmacro MUI_LANGUAGE "English"
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
Function SkipIfUpdate
|
||||||
|
${If} $IS_UPDATE == "1"
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
StrCpy $DATADIR "$APPDATA\LOGAR"
|
StrCpy $DATADIR "$APPDATA\LOGAR"
|
||||||
StrCpy $CFGFILE ""
|
StrCpy $CFGFILE ""
|
||||||
|
StrCpy $IS_UPDATE "0"
|
||||||
|
|
||||||
|
# 1. Mutual Exclusion Check: Deny installation if LOGAR Server is present
|
||||||
|
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Server" "DataDir"
|
||||||
|
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Server" "InstallDir"
|
||||||
|
${If} $0 != ""
|
||||||
|
${OrIf} $1 != ""
|
||||||
|
${OrIf} ${FileExists} "$PROGRAMFILES64\LOGAR-Server\Server.exe"
|
||||||
|
${OrIf} ${FileExists} "$PROGRAMFILES\LOGAR-Server\Server.exe"
|
||||||
|
MessageBox MB_ICONSTOP|MB_OK "LOGAR Server is already installed on this machine.$\r$\n$\r$\nLOGAR Client cannot be co-installed on a LOGAR Server host (LOGAR Server already provides built-in log collection).$\r$\n$\r$\nPlease uninstall LOGAR Server first if you wish to run a standalone forwarder client."
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
# 2. Existing LOGAR Client Detection for In-Place Update Mode
|
||||||
|
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Client" "DataDir"
|
||||||
|
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Client" "InstallDir"
|
||||||
|
${If} $0 != ""
|
||||||
|
${OrIf} $1 != ""
|
||||||
|
${OrIf} ${FileExists} "$INSTDIR\Win_Client.exe"
|
||||||
|
StrCpy $IS_UPDATE "1"
|
||||||
|
${If} $0 != ""
|
||||||
|
StrCpy $DATADIR $0
|
||||||
|
${EndIf}
|
||||||
|
${If} $1 != ""
|
||||||
|
StrCpy $INSTDIR $1
|
||||||
|
${EndIf}
|
||||||
|
MessageBox MB_ICONINFORMATION|MB_OK "An existing installation of LOGAR Client was detected.$\r$\n$\r$\nSetup will perform an in-place update: program files will be updated while your existing configuration (client_config.json), state tracking, and certificates are preserved."
|
||||||
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
Function ConfigPageCreate
|
Function ConfigPageCreate
|
||||||
|
${If} $IS_UPDATE == "1"
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
nsDialogs::Create 1018
|
nsDialogs::Create 1018
|
||||||
Pop $CfgDialog
|
Pop $CfgDialog
|
||||||
${If} $CfgDialog == error
|
${If} $CfgDialog == error
|
||||||
@@ -129,8 +171,9 @@ Section "Install"
|
|||||||
CreateDirectory "$DATADIR"
|
CreateDirectory "$DATADIR"
|
||||||
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
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 client config file was selected and not updating existing config, copy to data directory
|
||||||
${If} $CFGFILE != ""
|
${If} $IS_UPDATE != "1"
|
||||||
|
${AndIf} $CFGFILE != ""
|
||||||
${AndIf} ${FileExists} "$CFGFILE"
|
${AndIf} ${FileExists} "$CFGFILE"
|
||||||
CopyFiles /SILENT "$CFGFILE" "$DATADIR\client_config.json"
|
CopyFiles /SILENT "$CFGFILE" "$DATADIR\client_config.json"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
@@ -144,6 +187,7 @@ Section "Install"
|
|||||||
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
|
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
|
||||||
|
|
||||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "InstallDir" "$INSTDIR"
|
||||||
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "DataDir" "$DATADIR"
|
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "DataDir" "$DATADIR"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
[Setup]
|
[Setup]
|
||||||
|
AppId={{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}
|
||||||
AppName=LOGAR Server
|
AppName=LOGAR Server
|
||||||
AppVersion=0.3.2
|
AppVersion=0.3.4
|
||||||
AppPublisher=LOGAR Security Systems
|
AppPublisher=LOGAR
|
||||||
AppPublisherURL=https://logar.eibl.tech
|
AppPublisherURL=https://logar.eibl.tech
|
||||||
AppSupportURL=https://logar.eibl.tech
|
AppSupportURL=https://logar.eibl.tech
|
||||||
AppUpdatesURL=https://logar.eibl.tech
|
AppUpdatesURL=https://logar.eibl.tech
|
||||||
DefaultDirName={autopf}\LOGAR-Server
|
DefaultDirName={autopf}\LOGAR-Server
|
||||||
|
UsePreviousAppDir=yes
|
||||||
OutputDir=..\dist
|
OutputDir=..\dist
|
||||||
OutputBaseFilename=LOGAR-Server-Setup
|
OutputBaseFilename=LOGAR-Server-Setup
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=admin
|
||||||
@@ -15,11 +17,11 @@ WizardStyle=modern
|
|||||||
SetupIconFile=assets\logo.ico
|
SetupIconFile=assets\logo.ico
|
||||||
WizardImageFile=assets\wizard_sidebar.bmp
|
WizardImageFile=assets\wizard_sidebar.bmp
|
||||||
WizardSmallImageFile=assets\wizard_small.bmp
|
WizardSmallImageFile=assets\wizard_small.bmp
|
||||||
VersionInfoCompany=LOGAR Security Systems
|
VersionInfoCompany=LOGAR
|
||||||
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
|
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
|
||||||
VersionInfoVersion=0.3.2.0
|
VersionInfoVersion=0.3.4.0
|
||||||
VersionInfoProductName=LOGAR Server
|
VersionInfoProductName=LOGAR Server
|
||||||
VersionInfoProductVersion=0.3.2
|
VersionInfoProductVersion=0.3.4
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
@@ -54,6 +56,58 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server"
|
|||||||
[Code]
|
[Code]
|
||||||
var
|
var
|
||||||
DataDirPage: TInputDirWizardPage;
|
DataDirPage: TInputDirWizardPage;
|
||||||
|
IsUpdateMode: Boolean;
|
||||||
|
ExistingDataDir: String;
|
||||||
|
|
||||||
|
function InitializeSetup(): Boolean;
|
||||||
|
var
|
||||||
|
ClientFound: Boolean;
|
||||||
|
Val: String;
|
||||||
|
begin
|
||||||
|
Result := True;
|
||||||
|
IsUpdateMode := False;
|
||||||
|
ExistingDataDir := '';
|
||||||
|
ClientFound := False;
|
||||||
|
|
||||||
|
// 1. Mutual Exclusion Check: Deny installation if LOGAR Client is already present
|
||||||
|
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client') or
|
||||||
|
RegValueExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{9C4A6F1D-8975-476E-8512-4217DE029A88}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Client_is1') or
|
||||||
|
FileExists(ExpandConstant('{autopf}\LOGAR\Win_Client.exe')) then
|
||||||
|
begin
|
||||||
|
ClientFound := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if ClientFound then
|
||||||
|
begin
|
||||||
|
MsgBox('LOGAR Client is already installed on this machine.'#13#10#13#10 +
|
||||||
|
'LOGAR Server cannot be installed on a host where LOGAR Client is present (LOGAR Server already includes built-in local log collection).'#13#10#13#10 +
|
||||||
|
'Please uninstall LOGAR Client before installing LOGAR Server.',
|
||||||
|
mbCriticalError, MB_OK);
|
||||||
|
Result := False;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 2. Existing LOGAR Server Detection for In-Place Update Mode
|
||||||
|
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
|
||||||
|
RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\LOGAR Server_is1') or
|
||||||
|
FileExists(ExpandConstant('{autopf}\LOGAR-Server\Server.exe')) then
|
||||||
|
begin
|
||||||
|
IsUpdateMode := True;
|
||||||
|
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', Val) then
|
||||||
|
ExistingDataDir := Val;
|
||||||
|
|
||||||
|
MsgBox('An existing installation of LOGAR Server was detected.'#13#10#13#10 +
|
||||||
|
'Setup will perform an in-place update: core binaries will be updated, while your existing configuration (server_config.json), database (logar_state.db), and certificates will be preserved.',
|
||||||
|
mbInformation, MB_OK);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure InitializeWizard;
|
procedure InitializeWizard;
|
||||||
begin
|
begin
|
||||||
@@ -67,11 +121,29 @@ begin
|
|||||||
''
|
''
|
||||||
);
|
);
|
||||||
DataDirPage.Add('Data Directory:');
|
DataDirPage.Add('Data Directory:');
|
||||||
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
|
if (ExistingDataDir <> '') then
|
||||||
|
DataDirPage.Values[0] := ExistingDataDir
|
||||||
|
else
|
||||||
|
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if IsUpdateMode then
|
||||||
|
begin
|
||||||
|
if (PageID = wpSelectDir) or (PageID = DataDirPage.ID) then
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function GetDataDir(Param: String): String;
|
function GetDataDir(Param: String): String;
|
||||||
begin
|
begin
|
||||||
|
if IsUpdateMode and (ExistingDataDir <> '') then
|
||||||
|
begin
|
||||||
|
Result := ExistingDataDir;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
Result := DataDirPage.Values[0];
|
Result := DataDirPage.Values[0];
|
||||||
if Result = '' then
|
if Result = '' then
|
||||||
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
|
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||||
@@ -91,6 +163,7 @@ begin
|
|||||||
end
|
end
|
||||||
else if CurStep = ssPostInstall then
|
else if CurStep = ssPostInstall then
|
||||||
begin
|
begin
|
||||||
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'InstallDir', ExpandConstant('{app}'));
|
||||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -8,23 +8,25 @@ RequestExecutionLevel admin
|
|||||||
|
|
||||||
BrandingText "LOGAR Centralized Log Aggregator & Ingestion Hub"
|
BrandingText "LOGAR Centralized Log Aggregator & Ingestion Hub"
|
||||||
|
|
||||||
VIProductVersion "0.3.2.0"
|
VIProductVersion "0.3.4.0"
|
||||||
VIAddVersionKey "ProductName" "LOGAR Server"
|
VIAddVersionKey "ProductName" "LOGAR Server"
|
||||||
VIAddVersionKey "CompanyName" "LOGAR Security Systems"
|
VIAddVersionKey "CompanyName" "LOGAR"
|
||||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR Security Systems"
|
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
||||||
VIAddVersionKey "FileDescription" "LOGAR Cloud Hub & TCP Ingestion Server Setup"
|
VIAddVersionKey "FileDescription" "LOGAR Cloud Hub & TCP Ingestion Server Setup"
|
||||||
VIAddVersionKey "FileVersion" "0.3.2.0"
|
VIAddVersionKey "FileVersion" "0.3.4.0"
|
||||||
VIAddVersionKey "ProductVersion" "0.3.2.0"
|
VIAddVersionKey "ProductVersion" "0.3.4.0"
|
||||||
|
|
||||||
# --- Visual Graphics & Branding ---
|
# --- Visual Graphics & Branding ---
|
||||||
!define MUI_ICON "assets/logo.ico"
|
!define MUI_ICON "assets/logo.ico"
|
||||||
!define MUI_UNICON "assets/logo.ico"
|
!define MUI_UNICON "assets/logo.ico"
|
||||||
!define MUI_HEADERIMAGE
|
!define MUI_HEADERIMAGE
|
||||||
|
!define MUI_HEADERIMAGE_RIGHT
|
||||||
!define MUI_HEADERIMAGE_BITMAP "assets/wizard_header.bmp"
|
!define MUI_HEADERIMAGE_BITMAP "assets/wizard_header.bmp"
|
||||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
||||||
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
||||||
|
|
||||||
Var DATADIR
|
Var DATADIR
|
||||||
|
Var IS_UPDATE
|
||||||
|
|
||||||
# --- UI Pages ---
|
# --- UI Pages ---
|
||||||
!define MUI_ABORTWARNING
|
!define MUI_ABORTWARNING
|
||||||
@@ -37,6 +39,7 @@ Var DATADIR
|
|||||||
# Directory Page 1: Installation Directory
|
# Directory Page 1: Installation Directory
|
||||||
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
||||||
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||||
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ Var DATADIR
|
|||||||
!define MUI_PAGE_HEADER_SUBTEXT "Choose where database, certificates, and configuration files should be stored."
|
!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_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_TEXT_DESTINATION "Data & Storage Folder"
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||||
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
@@ -62,9 +66,43 @@ Var DATADIR
|
|||||||
|
|
||||||
!insertmacro MUI_LANGUAGE "English"
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
Function SkipIfUpdate
|
||||||
|
${If} $IS_UPDATE == "1"
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
|
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
|
||||||
|
StrCpy $IS_UPDATE "0"
|
||||||
|
|
||||||
|
# 1. Mutual Exclusion Check: Deny installation if LOGAR Client is present
|
||||||
|
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Client" "DataDir"
|
||||||
|
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Client" "InstallDir"
|
||||||
|
${If} $0 != ""
|
||||||
|
${OrIf} $1 != ""
|
||||||
|
${OrIf} ${FileExists} "$PROGRAMFILES64\LOGAR\Win_Client.exe"
|
||||||
|
${OrIf} ${FileExists} "$PROGRAMFILES\LOGAR\Win_Client.exe"
|
||||||
|
MessageBox MB_ICONSTOP|MB_OK "LOGAR Client is already installed on this machine.$\r$\n$\r$\nLOGAR Server cannot be installed on a host where LOGAR Client is present (LOGAR Server already includes built-in local log collection).$\r$\n$\r$\nPlease uninstall LOGAR Client before installing LOGAR Server."
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
# 2. Existing LOGAR Server Detection for In-Place Update Mode
|
||||||
|
ReadRegStr $0 HKLM "Software\LOGAR\LOGAR-Server" "DataDir"
|
||||||
|
ReadRegStr $1 HKLM "Software\LOGAR\LOGAR-Server" "InstallDir"
|
||||||
|
${If} $0 != ""
|
||||||
|
${OrIf} $1 != ""
|
||||||
|
${OrIf} ${FileExists} "$INSTDIR\Server.exe"
|
||||||
|
StrCpy $IS_UPDATE "1"
|
||||||
|
${If} $0 != ""
|
||||||
|
StrCpy $DATADIR $0
|
||||||
|
${EndIf}
|
||||||
|
${If} $1 != ""
|
||||||
|
StrCpy $INSTDIR $1
|
||||||
|
${EndIf}
|
||||||
|
MessageBox MB_ICONINFORMATION|MB_OK "An existing installation of LOGAR Server was detected.$\r$\n$\r$\nSetup will perform an in-place update: program files will be updated while your existing database, configuration (server_config.json), and certificates are preserved."
|
||||||
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
Section "Install"
|
Section "Install"
|
||||||
@@ -94,6 +132,7 @@ Section "Install"
|
|||||||
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
|
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
|
||||||
|
|
||||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "InstallDir" "$INSTDIR"
|
||||||
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "DataDir" "$DATADIR"
|
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "DataDir" "$DATADIR"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
# LOGAR Linux Edge Forwarder
|
|
||||||
|
|
||||||
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, 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 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.
|
|
||||||
- **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. 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:
|
|
||||||
```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 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..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> 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 (e.g. `/opt/logar/`):
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /opt/logar
|
|
||||||
sudo cp Linux_Client.bin client_config.json /opt/logar/
|
|
||||||
sudo chmod +x /opt/logar/Linux_Client.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| `--config` | `client_config.json` | Path to client configuration file |
|
|
||||||
| `--hours` | `24` | Lookback window in hours for journal logs |
|
|
||||||
| `--state-file` | `client_state.json` | Path to persistent state file |
|
|
||||||
| `--no-state` | `False` | Disable state tracking and send all events matching lookback window |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Manual Systemd Service & Timer Setup
|
|
||||||
|
|
||||||
### Step 1: Create the Systemd Service Unit
|
|
||||||
Create `/etc/systemd/system/logar-client.service`:
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=LOGAR Edge Log Forwarder
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=/opt/logar
|
|
||||||
ExecStart=/opt/logar/Linux_Client.bin --hours 24
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5s
|
|
||||||
User=root
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Enable and Start the Service
|
|
||||||
```bash
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now logar-client.service
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Check Logs
|
|
||||||
```bash
|
|
||||||
sudo journalctl -u logar-client.service -n 50 -f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Uninstallation & Removal
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
```
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"server_host": "192.168.1.100",
|
|
||||||
"server_port": 9443,
|
|
||||||
"server_fingerprint": "PASTE_SERVER_FINGERPRINT_HERE",
|
|
||||||
"server_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
|
||||||
"auth_token": "PASTE_AUTH_TOKEN_HERE"
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
# LOGAR Linux Server Hub
|
|
||||||
|
|
||||||
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, 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.
|
|
||||||
- **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. 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. 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
|
|
||||||
./Server.bin
|
|
||||||
```
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
[!] Config 'server_config.json' not found. Initializing first-run configuration...
|
|
||||||
[+] 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",
|
|
||||||
"tcp_host": "0.0.0.0",
|
|
||||||
"tcp_port": 9443,
|
|
||||||
"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": 1,
|
|
||||||
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
|
|
||||||
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
|
||||||
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Fleet Licensing, .lic Files & WebUI Dashboard
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
### CLI Licensing Utilities
|
|
||||||
Export a `.lic` request file via CLI:
|
|
||||||
```bash
|
|
||||||
./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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Manual Systemd Service Management
|
|
||||||
|
|
||||||
Check service status:
|
|
||||||
```bash
|
|
||||||
sudo systemctl status logar-server.service
|
|
||||||
```
|
|
||||||
|
|
||||||
Inspect live service logs:
|
|
||||||
```bash
|
|
||||||
sudo journalctl -u logar-server.service -f -n 50
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Uninstallation & Removal
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"server_name": "LOGAR-Linux-Hub",
|
|
||||||
"tcp_host": "0.0.0.0",
|
|
||||||
"tcp_port": 9443,
|
|
||||||
"hermes_host": "0.0.0.0",
|
|
||||||
"hermes_port": 8443,
|
|
||||||
"auth_token": "replace_with_secure_random_hex_token",
|
|
||||||
"db_path": "logar_state.db",
|
|
||||||
"evaluation_window_hours": 12,
|
|
||||||
"min_persistence_runs": 4,
|
|
||||||
"server_fingerprint": "AUTO_GENERATED_ON_FIRST_RUN",
|
|
||||||
"public_key": "AUTO_GENERATED_ON_FIRST_RUN",
|
|
||||||
"private_key": "AUTO_GENERATED_ON_FIRST_RUN"
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
# LOGAR Windows Edge Forwarder
|
|
||||||
|
|
||||||
Standalone compiled executable and installer distribution for Windows Server and workstation environments.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
`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`) 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.
|
|
||||||
- **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. 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:
|
|
||||||
```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 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..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The configuration contains **no host-specific names or site names** to ensure client anonymity and easy redistribution.
|
|
||||||
|
|
||||||
### Step 3: Copy to Edge Node
|
|
||||||
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\"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| `--config` | `client_config.json` | Path to client configuration file |
|
|
||||||
| `--hours` | `24` | Lookback window in hours for event logs |
|
|
||||||
| `--state-file` | `client_state.json` | Path to persistent state tracking file |
|
|
||||||
| `--no-state` | `False` | Disable state tracking and send all events matching lookback window |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
$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)
|
|
||||||
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 15)
|
|
||||||
|
|
||||||
Register-ScheduledTask -TaskName "LOGAR_Forwarder" `
|
|
||||||
-Action $Action `
|
|
||||||
-Trigger $Trigger `
|
|
||||||
-Settings $Settings `
|
|
||||||
-User "NT AUTHORITY\SYSTEM" `
|
|
||||||
-RunLevel Highest `
|
|
||||||
-Description "LOGAR Windows Edge Log Forwarder Service"
|
|
||||||
|
|
||||||
Start-ScheduledTask -TaskName "LOGAR_Forwarder"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Uninstallation
|
|
||||||
|
|
||||||
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
|
|
||||||
.\nssm.exe stop LOGAR_Client
|
|
||||||
.\nssm.exe remove LOGAR_Client confirm
|
|
||||||
Remove-Item -Recurse -Force "C:\LOGAR"
|
|
||||||
```
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"server_host": "192.168.1.100",
|
|
||||||
"server_port": 9443,
|
|
||||||
"server_fingerprint": "PASTE_SERVER_FINGERPRINT_HERE",
|
|
||||||
"server_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
|
||||||
"auth_token": "PASTE_AUTH_TOKEN_HERE"
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
# LOGAR Windows Server Hub
|
|
||||||
|
|
||||||
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, 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`) 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. 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 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
|
|
||||||
.\Server.exe
|
|
||||||
```
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
[!] Config 'server_config.json' not found. Initializing first-run configuration...
|
|
||||||
[+] 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",
|
|
||||||
"tcp_host": "0.0.0.0",
|
|
||||||
"tcp_port": 9443,
|
|
||||||
"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": 1,
|
|
||||||
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
|
|
||||||
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
|
||||||
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Fleet Licensing, .lic Files & WebUI Dashboard
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
### CLI Licensing Utilities
|
|
||||||
Export a `.lic` request file via CLI:
|
|
||||||
```powershell
|
|
||||||
.\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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Manual Windows Service Setup (via NSSM)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Uninstallation
|
|
||||||
|
|
||||||
If installed via the Inno Setup installer, use **Windows Add/Remove Programs**.
|
|
||||||
|
|
||||||
If installed manually via NSSM:
|
|
||||||
```powershell
|
|
||||||
.\nssm.exe stop LOGAR_Server
|
|
||||||
.\nssm.exe remove LOGAR_Server confirm
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"server_name": "LOGAR-Windows-Hub",
|
|
||||||
"tcp_host": "0.0.0.0",
|
|
||||||
"tcp_port": 9443,
|
|
||||||
"hermes_host": "0.0.0.0",
|
|
||||||
"hermes_port": 8443,
|
|
||||||
"auth_token": "replace_with_secure_random_hex_token",
|
|
||||||
"db_path": "logar_state.db",
|
|
||||||
"evaluation_window_hours": 12,
|
|
||||||
"min_persistence_runs": 4,
|
|
||||||
"server_fingerprint": "AUTO_GENERATED_ON_FIRST_RUN",
|
|
||||||
"public_key": "AUTO_GENERATED_ON_FIRST_RUN",
|
|
||||||
"private_key": "AUTO_GENERATED_ON_FIRST_RUN"
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,8 @@ import asyncio
|
|||||||
import secrets
|
import secrets
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import base64
|
||||||
|
import urllib.parse
|
||||||
import time
|
import time
|
||||||
import html
|
import html
|
||||||
import warnings
|
import warnings
|
||||||
@@ -276,6 +278,89 @@ def change_user_password(username: str, new_password: str, db_path: str = DEFAUL
|
|||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
# --- Two-Factor Authentication (TOTP / RFC 6238) ---
|
||||||
|
|
||||||
|
def generate_totp_secret(length: int = 20) -> str:
|
||||||
|
"""Generates a cryptographically random Base32-encoded TOTP secret key."""
|
||||||
|
return base64.b32encode(secrets.token_bytes(length)).decode("utf-8").replace("=", "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_totp_token(secret: str, intervals_no: Optional[int] = None) -> str:
|
||||||
|
"""Calculates a 6-digit TOTP code for the given counter interval according to RFC 6238."""
|
||||||
|
if intervals_no is None:
|
||||||
|
intervals_no = int(time.time()) // 30
|
||||||
|
key = base64.b32decode(secret + "=" * ((8 - len(secret) % 8) % 8), casefold=True)
|
||||||
|
msg = struct.pack(">Q", intervals_no)
|
||||||
|
h = hmac.new(key, msg, hashlib.sha1).digest()
|
||||||
|
o = h[19] & 15
|
||||||
|
h_int = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
|
||||||
|
return f"{h_int:06d}"
|
||||||
|
|
||||||
|
|
||||||
|
def verify_totp_code(secret: str, code: str, window: int = 1) -> bool:
|
||||||
|
"""Verifies a 6-digit TOTP code with standard drift window (+/- 30s)."""
|
||||||
|
if not secret or not code:
|
||||||
|
return False
|
||||||
|
code = str(code).strip().replace(" ", "")
|
||||||
|
if len(code) != 6 or not code.isdigit():
|
||||||
|
return False
|
||||||
|
now = int(time.time())
|
||||||
|
current_step = now // 30
|
||||||
|
for offset in range(-window, window + 1):
|
||||||
|
if hmac.compare_digest(get_totp_token(secret, current_step + offset), code):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_totp_uri(username: str, secret: str, issuer: str = "LOGAR Server Hub") -> str:
|
||||||
|
"""Generates standard otpauth URI for QR code generation."""
|
||||||
|
user_enc = urllib.parse.quote(username)
|
||||||
|
issuer_enc = urllib.parse.quote(issuer)
|
||||||
|
return f"otpauth://totp/{issuer_enc}:{user_enc}?secret={secret}&issuer={issuer_enc}&algorithm=SHA1&digits=6&period=30"
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_totp_info(username: str, db_path: str = DEFAULT_DB_FILE) -> Dict[str, Any]:
|
||||||
|
"""Retrieves TOTP configuration for a user from the SQLite database."""
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
try:
|
||||||
|
c.execute("SELECT totp_secret, totp_enabled FROM users WHERE username = ?", (username,))
|
||||||
|
row = c.fetchone()
|
||||||
|
if row:
|
||||||
|
return {"secret": row[0] or "", "enabled": bool(row[1])}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return {"secret": "", "enabled": False}
|
||||||
|
|
||||||
|
|
||||||
|
def enable_user_totp(username: str, secret: str, db_path: str = DEFAULT_DB_FILE) -> bool:
|
||||||
|
"""Enables and activates TOTP for a user in the SQLite database."""
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
try:
|
||||||
|
c.execute("UPDATE users SET totp_secret = ?, totp_enabled = 1 WHERE username = ?", (secret, username))
|
||||||
|
changed = c.rowcount > 0
|
||||||
|
conn.commit()
|
||||||
|
return changed
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def disable_user_totp(username: str, db_path: str = DEFAULT_DB_FILE) -> bool:
|
||||||
|
"""Disables TOTP for a user in the SQLite database."""
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
try:
|
||||||
|
c.execute("UPDATE users SET totp_secret = NULL, totp_enabled = 0 WHERE username = ?", (username,))
|
||||||
|
changed = c.rowcount > 0
|
||||||
|
conn.commit()
|
||||||
|
return changed
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# --- Hermes Token Management ---
|
# --- Hermes Token Management ---
|
||||||
|
|
||||||
def create_hermes_token(name: str, token: Optional[str] = None, db_path: str = DEFAULT_DB_FILE) -> Dict[str, Any]:
|
def create_hermes_token(name: str, token: Optional[str] = None, db_path: str = DEFAULT_DB_FILE) -> Dict[str, Any]:
|
||||||
@@ -765,10 +850,22 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in
|
|||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
salt TEXT NOT NULL,
|
salt TEXT NOT NULL,
|
||||||
role TEXT NOT NULL DEFAULT 'admin',
|
role TEXT NOT NULL DEFAULT 'admin',
|
||||||
created_at TEXT NOT NULL
|
created_at TEXT NOT NULL,
|
||||||
|
totp_secret TEXT,
|
||||||
|
totp_enabled INTEGER DEFAULT 0
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Migrations for users table (2FA)
|
||||||
|
for col, col_def in [
|
||||||
|
("totp_secret", "TEXT"),
|
||||||
|
("totp_enabled", "INTEGER DEFAULT 0")
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
conn.execute(f"ALTER TABLE users ADD COLUMN {col} {col_def}")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Ensure default admin user exists
|
# Ensure default admin user exists
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT COUNT(*) FROM users")
|
c.execute("SELECT COUNT(*) FROM users")
|
||||||
@@ -1306,6 +1403,7 @@ def login_page(request: Request):
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LOGAR Hub - Login</title>
|
<title>LOGAR Hub - Login</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpolygon points='256,24 456,139 456,373 256,488 56,373 56,139' fill='%230E1A2E' stroke='%2300E5FF' stroke-width='24'/%3E%3Crect x='140' y='140' width='46' height='232' rx='10' fill='%2300E5FF'/%3E%3Crect x='140' y='326' width='220' height='46' rx='10' fill='%2300E5FF'/%3E%3Crect x='220' y='210' width='36' height='80' rx='8' fill='%2300FFA3'/%3E%3Crect x='286' y='150' width='36' height='140' rx='8' fill='%2300E5FF'/%3E%3C/svg%3E">
|
||||||
<style>
|
<style>
|
||||||
:root { --bg: #0b0f19; --card: #151c2e; --primary: #3b82f6; --primary-hover: #2563eb; --border: #1e293b; --text: #f8fafc; --text-muted: #94a3b8; --danger: #ef4444; }
|
:root { --bg: #0b0f19; --card: #151c2e; --primary: #3b82f6; --primary-hover: #2563eb; --border: #1e293b; --text: #f8fafc; --text-muted: #94a3b8; --danger: #ef4444; }
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@@ -1341,14 +1439,19 @@ def login_page(request: Request):
|
|||||||
</div>
|
</div>
|
||||||
<div id="loginError" class="alert-error"></div>
|
<div id="loginError" class="alert-error"></div>
|
||||||
<form id="loginForm" onsubmit="handleLogin(event)">
|
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||||
<div class="form-group">
|
<div class="form-group" id="userGroup">
|
||||||
<label class="form-label">Username</label>
|
<label class="form-label">Username</label>
|
||||||
<input type="text" id="username" class="form-control" placeholder="admin" required autofocus>
|
<input type="text" id="username" class="form-control" placeholder="admin" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" id="passGroup">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label">Password</label>
|
||||||
<input type="password" id="password" class="form-control" placeholder="••••••••" required>
|
<input type="password" id="password" class="form-control" placeholder="••••••••" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" id="totpGroup" style="display: none;">
|
||||||
|
<label class="form-label">🛡️ 2FA Verification Code</label>
|
||||||
|
<p style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">Enter the 6-digit code from your authenticator app:</p>
|
||||||
|
<input type="text" id="totpCode" class="form-control" placeholder="123456" maxlength="6" inputmode="numeric" style="font-family: 'JetBrains Mono', monospace; font-size: 18px; letter-spacing: 4px; text-align: center;">
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn" id="submitBtn">Sign In</button>
|
<button type="submit" class="btn" id="submitBtn">Sign In</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -1357,26 +1460,37 @@ def login_page(request: Request):
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const u = document.getElementById('username').value.trim();
|
const u = document.getElementById('username').value.trim();
|
||||||
const p = document.getElementById('password').value;
|
const p = document.getElementById('password').value;
|
||||||
|
const totp = document.getElementById('totpCode').value.trim();
|
||||||
const errEl = document.getElementById('loginError');
|
const errEl = document.getElementById('loginError');
|
||||||
const btn = document.getElementById('submitBtn');
|
const btn = document.getElementById('submitBtn');
|
||||||
|
const totpGrp = document.getElementById('totpGroup');
|
||||||
errEl.style.display = 'none';
|
errEl.style.display = 'none';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Authenticating...';
|
btn.textContent = 'Authenticating...';
|
||||||
|
|
||||||
|
const payload = { username: u, password: p };
|
||||||
|
if (totp) payload.totp_code = totp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/login', {
|
const res = await fetch('/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: u, password: p })
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.status === 'success') {
|
if (res.ok && data.status === 'success') {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
|
} else if (data.requires_2fa || data.status === '2fa_required') {
|
||||||
|
totpGrp.style.display = 'block';
|
||||||
|
document.getElementById('totpCode').focus();
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Verify & Sign In';
|
||||||
|
errEl.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
errEl.textContent = data.detail || 'Invalid username or password';
|
errEl.textContent = data.detail || 'Invalid username or password';
|
||||||
errEl.style.display = 'block';
|
errEl.style.display = 'block';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = 'Sign In';
|
btn.textContent = totpGrp.style.display === 'block' ? 'Verify & Sign In' : 'Sign In';
|
||||||
}
|
}
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
errEl.textContent = 'Connection error: ' + ex.message;
|
errEl.textContent = 'Connection error: ' + ex.message;
|
||||||
@@ -1392,24 +1506,26 @@ def login_page(request: Request):
|
|||||||
|
|
||||||
@app.post("/login")
|
@app.post("/login")
|
||||||
async def process_login(request: Request):
|
async def process_login(request: Request):
|
||||||
"""Processes login credentials and issues session cookie."""
|
"""Processes login credentials and issues session cookie with 2FA TOTP enforcement."""
|
||||||
client_ip = get_client_ip(request)
|
client_ip = get_client_ip(request)
|
||||||
if not check_ip_rate_limit(client_ip, _LOGIN_FAILURES, max_attempts=10, window_seconds=60):
|
if not check_ip_rate_limit(client_ip, _LOGIN_FAILURES, max_attempts=10, window_seconds=60):
|
||||||
raise HTTPException(status_code=429, detail="Too many failed login attempts. Please try again later.")
|
raise HTTPException(status_code=429, detail="Too many failed login attempts. Please try again later.")
|
||||||
|
|
||||||
username, password = None, None
|
username, password, totp_code = None, None, None
|
||||||
content_type = request.headers.get("content-type", "")
|
content_type = request.headers.get("content-type", "")
|
||||||
if "application/json" in content_type:
|
if "application/json" in content_type:
|
||||||
try:
|
try:
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
username = body.get("username")
|
username = body.get("username")
|
||||||
password = body.get("password")
|
password = body.get("password")
|
||||||
|
totp_code = body.get("totp_code")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
username = form.get("username")
|
username = form.get("username")
|
||||||
password = form.get("password")
|
password = form.get("password")
|
||||||
|
totp_code = form.get("totp_code")
|
||||||
|
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
raise HTTPException(status_code=400, detail="Username and password are required")
|
raise HTTPException(status_code=400, detail="Username and password are required")
|
||||||
@@ -1420,6 +1536,20 @@ async def process_login(request: Request):
|
|||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||||
|
|
||||||
|
# Check 2FA TOTP requirement
|
||||||
|
totp_info = get_user_totp_info(username, db_path=db_path)
|
||||||
|
if totp_info.get("enabled"):
|
||||||
|
if not totp_code:
|
||||||
|
return JSONResponse(status_code=200, content={
|
||||||
|
"status": "2fa_required",
|
||||||
|
"requires_2fa": True,
|
||||||
|
"message": "Two-factor authentication code required"
|
||||||
|
})
|
||||||
|
if not verify_totp_code(totp_info["secret"], totp_code):
|
||||||
|
record_failed_ip_attempt(client_ip, _LOGIN_FAILURES)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid 6-digit 2FA verification code")
|
||||||
|
|
||||||
reset_failed_ip_attempts(client_ip, _LOGIN_FAILURES)
|
reset_failed_ip_attempts(client_ip, _LOGIN_FAILURES)
|
||||||
token = generate_session_token(username)
|
token = generate_session_token(username)
|
||||||
is_https = request.url.scheme == "https" or request.headers.get("x-forwarded-proto") == "https"
|
is_https = request.url.scheme == "https" or request.headers.get("x-forwarded-proto") == "https"
|
||||||
@@ -1455,6 +1585,55 @@ async def api_change_password(req: ChangePasswordRequest, current_user: str = De
|
|||||||
return {"status": "success", "message": "Password updated successfully"}
|
return {"status": "success", "message": "Password updated successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
# --- 2FA Authentication Management Endpoints ---
|
||||||
|
|
||||||
|
@app.get("/api/user/2fa/status")
|
||||||
|
def api_user_2fa_status(auth_user: str = Depends(require_admin_auth)):
|
||||||
|
"""Returns 2FA status for currently logged in administrator."""
|
||||||
|
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||||
|
info = get_user_totp_info(auth_user, db_path=db_path)
|
||||||
|
return {"enabled": info["enabled"], "username": auth_user}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/user/2fa/setup")
|
||||||
|
def api_user_2fa_setup(auth_user: str = Depends(require_admin_auth)):
|
||||||
|
"""Generates a new TOTP secret and otpauth URI for 2FA provisioning."""
|
||||||
|
secret = generate_totp_secret()
|
||||||
|
otpauth = get_totp_uri(auth_user, secret, issuer="LOGAR Server Hub")
|
||||||
|
return {
|
||||||
|
"secret": secret,
|
||||||
|
"otpauth_uri": otpauth,
|
||||||
|
"username": auth_user
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/user/2fa/enable")
|
||||||
|
async def api_user_2fa_enable(request: Request, auth_user: str = Depends(require_admin_auth)):
|
||||||
|
"""Enables TOTP for the current user after validating 6-digit code."""
|
||||||
|
body = await request.json()
|
||||||
|
secret = body.get("secret", "").strip()
|
||||||
|
code = body.get("code", "").strip()
|
||||||
|
if not secret or not code:
|
||||||
|
raise HTTPException(status_code=400, detail="Secret and 6-digit verification code required")
|
||||||
|
if not verify_totp_code(secret, code):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid 6-digit TOTP verification code. Please check your device clock.")
|
||||||
|
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||||
|
enable_user_totp(auth_user, secret, db_path=db_path)
|
||||||
|
return {"status": "success", "message": "2FA successfully activated"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/user/2fa/disable")
|
||||||
|
async def api_user_2fa_disable(request: Request, auth_user: str = Depends(require_admin_auth)):
|
||||||
|
"""Disables TOTP for the current user after verifying password."""
|
||||||
|
body = await request.json()
|
||||||
|
password = body.get("password", "")
|
||||||
|
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||||
|
if not verify_user_credentials(auth_user, password, db_path=db_path):
|
||||||
|
raise HTTPException(status_code=403, detail="Current password incorrect")
|
||||||
|
disable_user_totp(auth_user, db_path=db_path)
|
||||||
|
return {"status": "success", "message": "2FA successfully disabled"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/client/enroll")
|
@app.post("/api/client/enroll")
|
||||||
def enroll_client(req: ClientEnrollRequest, request: Request = None):
|
def enroll_client(req: ClientEnrollRequest, request: Request = None):
|
||||||
"""
|
"""
|
||||||
@@ -2241,6 +2420,7 @@ def serve_webui(request: Request = None):
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LOGAR Hub - Fleet Aggregation & Management</title>
|
<title>LOGAR Hub - Fleet Aggregation & Management</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpolygon points='256,24 456,139 456,373 256,488 56,373 56,139' fill='%230E1A2E' stroke='%2300E5FF' stroke-width='24'/%3E%3Crect x='140' y='140' width='46' height='232' rx='10' fill='%2300E5FF'/%3E%3Crect x='140' y='326' width='220' height='46' rx='10' fill='%2300E5FF'/%3E%3Crect x='220' y='210' width='36' height='80' rx='8' fill='%2300FFA3'/%3E%3Crect x='286' y='150' width='36' height='140' rx='8' fill='%2300E5FF'/%3E%3C/svg%3E">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root {{
|
:root {{
|
||||||
@@ -2301,7 +2481,7 @@ def serve_webui(request: Request = None):
|
|||||||
border-radius: 10px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
|
border-radius: 10px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
|
||||||
min-width: 200px; z-index: 200; overflow: hidden;
|
min-width: 200px; z-index: 200; overflow: hidden;
|
||||||
}}
|
}}
|
||||||
.user-menu-container:hover .user-dropdown, .user-menu-container:focus-within .user-dropdown {{ display: block; }}
|
.user-menu-container:hover .user-dropdown, .user-menu-container:focus-within .user-dropdown, .user-dropdown.show {{ display: block; }}
|
||||||
.user-dropdown-header {{ padding: 10px 14px; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); background: #0f172a; border-bottom: 1px solid var(--border); }}
|
.user-dropdown-header {{ padding: 10px 14px; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); background: #0f172a; border-bottom: 1px solid var(--border); }}
|
||||||
.user-dropdown-item {{
|
.user-dropdown-item {{
|
||||||
display: flex; align-items: center; gap: 8px; padding: 10px 14px;
|
display: flex; align-items: center; gap: 8px; padding: 10px 14px;
|
||||||
@@ -2440,13 +2620,14 @@ def serve_webui(request: Request = None):
|
|||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button onclick="openExportProfileModal()" class="btn btn-primary">📥 Export Client Profile</button>
|
<button onclick="openExportProfileModal()" class="btn btn-primary">📥 Export Client Profile</button>
|
||||||
<div class="user-menu-container" tabindex="0">
|
<div class="user-menu-container" tabindex="0">
|
||||||
<div class="user-tag">
|
<div class="user-tag" onclick="toggleUserDropdown(event)">
|
||||||
<span>👤 <strong>{auth_user}</strong></span>
|
<span>👤 <strong>{auth_user}</strong></span>
|
||||||
<span style="font-size: 10px; color: var(--text-muted);">▼</span>
|
<span style="font-size: 10px; color: var(--text-muted);">▼</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-dropdown">
|
<div class="user-dropdown" id="userDropdown">
|
||||||
<div class="user-dropdown-header">Logged in as {auth_user}</div>
|
<div class="user-dropdown-header">Logged in as {auth_user}</div>
|
||||||
<button class="user-dropdown-item" onclick="openPasswordModal()">🔑 Change Password</button>
|
<button class="user-dropdown-item" onclick="openTwoFactorModal(); hideUserDropdown();">🛡️ Two-Factor Auth (2FA)</button>
|
||||||
|
<button class="user-dropdown-item" onclick="openPasswordModal(); hideUserDropdown();">🔑 Change Password</button>
|
||||||
<a href="/logout" class="user-dropdown-item danger">🚪 Logout</a>
|
<a href="/logout" class="user-dropdown-item danger">🚪 Logout</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2581,7 +2762,7 @@ def serve_webui(request: Request = None):
|
|||||||
<div class="tab-pane" id="tabPane-anomalies">
|
<div class="tab-pane" id="tabPane-anomalies">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title" style="font-size: 15px;">Verified Anomaly Stream (Rolling 12h Window)</span>
|
<span class="card-title" style="font-size: 15px;">Verified Anomaly Stream (Latest 50 Events • Rolling 12h Window)</span>
|
||||||
<span class="badge badge-active">Direct Hermes Intelligence</span>
|
<span class="badge badge-active">Direct Hermes Intelligence</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow-x: auto;">
|
<div style="overflow-x: auto;">
|
||||||
@@ -2730,7 +2911,92 @@ def serve_webui(request: Request = None):
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Two-Factor Authentication Modal -->
|
||||||
|
<div id="twoFactorModal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 480px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 14px;">
|
||||||
|
<div class="modal-title" style="margin-bottom: 0; display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span>🛡️</span> Two-Factor Authentication (2FA)
|
||||||
|
</div>
|
||||||
|
<div style="cursor: pointer; font-size: 22px; color: var(--text-muted); line-height: 1;" onclick="closeModal('twoFactorModal')">×</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="twoFactorLoading" style="text-align: center; padding: 24px; color: var(--text-muted);">
|
||||||
|
Loading 2FA security status...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2FA Active Section -->
|
||||||
|
<div id="twoFactorActiveSection" style="display: none;">
|
||||||
|
<div style="background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 8px; padding: 16px; margin-bottom: 20px; display: flex; align-items: center; gap: 12px;">
|
||||||
|
<div style="font-size: 24px;">🟢</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; color: #34d399;">2FA is Currently Active</div>
|
||||||
|
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">Your account is secured with TOTP authenticator verification.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px; background: #0f172a; padding: 16px; border-radius: 8px; border: 1px solid var(--border);">
|
||||||
|
<div style="font-size: 13px; font-weight: 700; color: #ef4444; margin-bottom: 8px;">Disable Two-Factor Authentication</div>
|
||||||
|
<p style="font-size: 12px; color: var(--text-muted); margin-bottom: 12px;">Enter your account password to confirm disabling 2FA:</p>
|
||||||
|
<div class="form-group" style="margin-bottom: 12px;">
|
||||||
|
<input type="password" id="twoFactorDisablePassword" class="form-control" placeholder="Current Account Password">
|
||||||
|
</div>
|
||||||
|
<button onclick="submitDisable2FA()" class="btn btn-danger" style="width: 100%;">Disable 2FA</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 16px;">
|
||||||
|
<button onclick="start2FASetup()" class="btn btn-outline" style="font-size: 12px;">🔄 Re-configure on New Device</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions" style="margin-top: 20px;">
|
||||||
|
<button onclick="closeModal('twoFactorModal')" class="btn btn-primary">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2FA Setup Section -->
|
||||||
|
<div id="twoFactorSetupSection" style="display: none;">
|
||||||
|
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 16px;">
|
||||||
|
Scan this QR code with an authenticator app (Google Authenticator, Authy, 1Password, Bitwarden, etc.):
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: center; align-items: center; background: #ffffff; padding: 16px; border-radius: 12px; margin-bottom: 16px; min-height: 190px;">
|
||||||
|
<div id="qrcodeCanvas"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #0f172a; padding: 12px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 18px;">
|
||||||
|
<div style="font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px;">Manual Setup Secret Key</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
|
||||||
|
<code id="twoFactorSecretText" style="font-size: 13px; color: #60a5fa; word-break: break-all; user-select: all;"></code>
|
||||||
|
<button onclick="copy2FASecret()" class="btn btn-outline" style="padding: 4px 8px; font-size: 11px; white-space: nowrap;">📋 Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 18px;">
|
||||||
|
<label class="form-label">Enter 6-Digit Authenticator Code</label>
|
||||||
|
<input type="text" id="twoFactorVerifyCode" class="form-control" placeholder="123456" maxlength="6" inputmode="numeric" style="text-align: center; letter-spacing: 4px; font-size: 20px; font-family: 'JetBrains Mono', monospace; font-weight: 700;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button onclick="closeModal('twoFactorModal')" class="btn btn-outline">Cancel</button>
|
||||||
|
<button onclick="submitEnable2FA()" class="btn btn-accent">✅ Verify & Activate 2FA</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function toggleUserDropdown(e) {{
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
const d = document.getElementById('userDropdown');
|
||||||
|
if (d) d.classList.toggle('show');
|
||||||
|
}}
|
||||||
|
|
||||||
|
function hideUserDropdown() {{
|
||||||
|
const d = document.getElementById('userDropdown');
|
||||||
|
if (d) d.classList.remove('show');
|
||||||
|
}}
|
||||||
|
|
||||||
|
window.addEventListener('click', () => hideUserDropdown());
|
||||||
function switchTab(tabId) {{
|
function switchTab(tabId) {{
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'));
|
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'));
|
||||||
@@ -2946,6 +3212,123 @@ def serve_webui(request: Request = None):
|
|||||||
alert(data.detail || 'Failed to change password');
|
alert(data.detail || 'Failed to change password');
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// --- 2FA Management & QRCode Logic ---
|
||||||
|
let current2FASecret = '';
|
||||||
|
|
||||||
|
async function openTwoFactorModal() {{
|
||||||
|
openModal('twoFactorModal');
|
||||||
|
document.getElementById('twoFactorLoading').style.display = 'block';
|
||||||
|
document.getElementById('twoFactorActiveSection').style.display = 'none';
|
||||||
|
document.getElementById('twoFactorSetupSection').style.display = 'none';
|
||||||
|
|
||||||
|
try {{
|
||||||
|
const res = await fetch('/api/user/2fa/status');
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('twoFactorLoading').style.display = 'none';
|
||||||
|
if (data.enabled) {{
|
||||||
|
document.getElementById('twoFactorActiveSection').style.display = 'block';
|
||||||
|
}} else {{
|
||||||
|
start2FASetup();
|
||||||
|
}}
|
||||||
|
}} catch(ex) {{
|
||||||
|
document.getElementById('twoFactorLoading').textContent = 'Failed to load 2FA status: ' + ex.message;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
async function start2FASetup() {{
|
||||||
|
document.getElementById('twoFactorLoading').style.display = 'block';
|
||||||
|
document.getElementById('twoFactorActiveSection').style.display = 'none';
|
||||||
|
document.getElementById('twoFactorSetupSection').style.display = 'none';
|
||||||
|
|
||||||
|
try {{
|
||||||
|
const res = await fetch('/api/user/2fa/setup');
|
||||||
|
const data = await res.json();
|
||||||
|
current2FASecret = data.secret;
|
||||||
|
|
||||||
|
document.getElementById('twoFactorSecretText').textContent = data.secret;
|
||||||
|
document.getElementById('twoFactorVerifyCode').value = '';
|
||||||
|
|
||||||
|
// Render QR code
|
||||||
|
renderQRCode('qrcodeCanvas', data.otpauth_uri);
|
||||||
|
|
||||||
|
document.getElementById('twoFactorLoading').style.display = 'none';
|
||||||
|
document.getElementById('twoFactorSetupSection').style.display = 'block';
|
||||||
|
}} catch(ex) {{
|
||||||
|
document.getElementById('twoFactorLoading').textContent = 'Failed to generate 2FA setup: ' + ex.message;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function copy2FASecret() {{
|
||||||
|
if (!current2FASecret) return;
|
||||||
|
navigator.clipboard.writeText(current2FASecret);
|
||||||
|
showAlert('2FA Secret copied to clipboard!');
|
||||||
|
}}
|
||||||
|
|
||||||
|
async function submitEnable2FA() {{
|
||||||
|
const code = document.getElementById('twoFactorVerifyCode').value.trim();
|
||||||
|
if (!code || code.length !== 6) {{
|
||||||
|
alert('Please enter a valid 6-digit verification code.');
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
try {{
|
||||||
|
const res = await fetch('/api/user/2fa/enable', {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {{ 'Content-Type': 'application/json' }},
|
||||||
|
body: JSON.stringify({{ secret: current2FASecret, code: code }})
|
||||||
|
}});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {{
|
||||||
|
showAlert('Two-factor authentication (2FA) successfully activated!');
|
||||||
|
closeModal('twoFactorModal');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}} else {{
|
||||||
|
alert(data.detail || 'Failed to verify code. Please check your authenticator clock.');
|
||||||
|
}}
|
||||||
|
}} catch(ex) {{
|
||||||
|
alert('Connection error: ' + ex.message);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
async function submitDisable2FA() {{
|
||||||
|
const pwd = document.getElementById('twoFactorDisablePassword').value;
|
||||||
|
if (!pwd) {{
|
||||||
|
alert('Please enter your account password to confirm.');
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
if (!confirm('Are you sure you want to disable two-factor authentication?')) return;
|
||||||
|
|
||||||
|
try {{
|
||||||
|
const res = await fetch('/api/user/2fa/disable', {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {{ 'Content-Type': 'application/json' }},
|
||||||
|
body: JSON.stringify({{ password: pwd }})
|
||||||
|
}});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {{
|
||||||
|
showAlert('2FA disabled successfully.');
|
||||||
|
closeModal('twoFactorModal');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}} else {{
|
||||||
|
alert(data.detail || 'Failed to disable 2FA. Incorrect password.');
|
||||||
|
}}
|
||||||
|
}} catch(ex) {{
|
||||||
|
alert('Connection error: ' + ex.message);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function renderQRCode(containerId, text) {{
|
||||||
|
const el = document.getElementById(containerId);
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = '';
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.style.width = '180px';
|
||||||
|
img.style.height = '180px';
|
||||||
|
img.style.display = 'block';
|
||||||
|
img.alt = '2FA QR Code';
|
||||||
|
img.src = 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodeURIComponent(text);
|
||||||
|
el.appendChild(img);
|
||||||
|
}}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
|
|||||||
@@ -470,6 +470,46 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
self.assertEqual(client_data["active_seats"], 0)
|
self.assertEqual(client_data["active_seats"], 0)
|
||||||
self.assertEqual(len(client_data["clients"]), 1)
|
self.assertEqual(len(client_data["clients"]), 1)
|
||||||
|
|
||||||
|
def test_two_factor_auth_and_totp_flow(self):
|
||||||
|
"""Tests TOTP secret generation, token verification, URI generation, and enable/disable lifecycle."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
|
||||||
|
# 1. Test TOTP helpers
|
||||||
|
secret = Server.generate_totp_secret()
|
||||||
|
self.assertTrue(len(secret) >= 16)
|
||||||
|
token = Server.get_totp_token(secret)
|
||||||
|
self.assertEqual(len(token), 6)
|
||||||
|
self.assertTrue(token.isdigit())
|
||||||
|
self.assertTrue(Server.verify_totp_code(secret, token))
|
||||||
|
self.assertFalse(Server.verify_totp_code(secret, "000000" if token != "000000" else "999999"))
|
||||||
|
self.assertFalse(Server.verify_totp_code(secret, "invalid"))
|
||||||
|
|
||||||
|
# 2. Test OTPAuth URI
|
||||||
|
uri = Server.get_totp_uri("admin", secret, issuer="LOGAR Fleet Hub")
|
||||||
|
self.assertTrue(uri.startswith("otpauth://totp/"))
|
||||||
|
self.assertIn("secret=" + secret, uri)
|
||||||
|
self.assertIn("LOGAR", uri)
|
||||||
|
|
||||||
|
# 3. Test initial user 2FA status
|
||||||
|
info = Server.get_user_totp_info("admin", db_path=self.test_db)
|
||||||
|
self.assertFalse(info["enabled"])
|
||||||
|
self.assertEqual(info["secret"], "")
|
||||||
|
|
||||||
|
# 4. Test enabling 2FA
|
||||||
|
enabled = Server.enable_user_totp("admin", secret, db_path=self.test_db)
|
||||||
|
self.assertTrue(enabled)
|
||||||
|
|
||||||
|
info_active = Server.get_user_totp_info("admin", db_path=self.test_db)
|
||||||
|
self.assertTrue(info_active["enabled"])
|
||||||
|
self.assertEqual(info_active["secret"], secret)
|
||||||
|
|
||||||
|
# 5. Test disabling 2FA
|
||||||
|
disabled = Server.disable_user_totp("admin", db_path=self.test_db)
|
||||||
|
self.assertTrue(disabled)
|
||||||
|
|
||||||
|
info_disabled = Server.get_user_totp_info("admin", db_path=self.test_db)
|
||||||
|
self.assertFalse(info_disabled["enabled"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||