Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa662da476 | ||
|
|
6cb9631958 | ||
|
|
c03bb348b5 | ||
|
|
2d39f3c007 | ||
|
|
78e62b31d2 | ||
|
|
860e39b360 | ||
|
|
2a04244db3 | ||
|
|
9f999fc6c3 | ||
|
|
3399c6d57b | ||
|
|
c6d04f8896 | ||
|
|
0a67b43619 | ||
|
|
3234acf435 | ||
|
|
702ca985d2 | ||
|
|
97d92d1bee | ||
|
|
a8cd2cd83c | ||
|
|
6843b22ac1 | ||
|
|
bfd576bfdd | ||
|
|
9810220cb7 |
+4
-3
@@ -22,14 +22,15 @@ out/
|
|||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
client_state.json
|
|
||||||
*.tmp
|
|
||||||
|
|
||||||
# Live configuration with generated private keys & tokens (samples are tracked)
|
# Live configuration with generated private keys & tokens (samples are tracked)
|
||||||
server_config.json
|
server_config.json
|
||||||
client_config.json
|
client_config.json
|
||||||
|
client_state.json
|
||||||
|
local_collector_state.json
|
||||||
test_*.json
|
test_*.json
|
||||||
test_*.db
|
test_*.db
|
||||||
|
test_pipeline_master_ca/
|
||||||
|
test_pipeline_certs/
|
||||||
master_ca_certs/
|
master_ca_certs/
|
||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
|
|||||||
+1
-1
Submodule LOGAR-LICENSING updated: 119061721a...38a41cfbac
@@ -1,14 +1,13 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="assets/logo_banner.svg" alt="LOGAR Banner" width="750"/>
|
<img src="https://gitea.eibl.tech/me0nline/LOGAR/raw/branch/main/assets/logo_banner.svg" alt="LOGAR Banner" width="750"/>
|
||||||
</p>
|
</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.
|
||||||
|
|
||||||
@@ -174,10 +173,10 @@ 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)
|
||||||
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
|
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
|
||||||
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
|
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
|
||||||
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
|
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
|
||||||
|
* [Branding & Design System Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||||
|
|||||||
@@ -1,5 +1,40 @@
|
|||||||
# LOGAR Release Notes
|
# LOGAR Release Notes
|
||||||
|
|
||||||
|
## [v0.3.4.2] - 2026-09-30
|
||||||
|
|
||||||
|
### 🔒 Exclusive Hardware Host-Binding & Reassignment Controls
|
||||||
|
- **Automated Host Binding on First Activation**: Licenses now automatically lock to the specific server hardware fingerprint (`server_fingerprint`) and hostname upon first activation/enrollment, preventing multi-host cloning or license hijacking.
|
||||||
|
- **Manual Host Reassignment Control**: Added `🔓 Reset Host Binding` button to the Master License Authority dashboard (`licenseDetailsModal`), allowing administrators to unbind a license so it can be cleanly transferred or reassigned to a new server instance.
|
||||||
|
- **REST & CLI Management**: Added `POST /api/license/reset-binding` and CLI argument `--reset-binding LIC_ID` for automated provisioning pipelines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [v0.3.4.1] - 2026-09-27
|
||||||
|
|
||||||
|
### 🎨 Installer Dialog UI Spacing & Branding Enhancements
|
||||||
|
- **NSIS Client Config Profile Dialog Fix**: Adjusted vertical coordinates and label boundaries in the Windows Client NSIS installer (`installer_client.nsi`) to prevent input field overlap and ensure multi-line instructional text renders without visual clipping.
|
||||||
|
- **Installer Footer Branding Clean-Up**: Escaped ampersands in Windows installer branding labels (`BrandingText`) to avoid character truncation in native win32 footer panels.
|
||||||
|
- **License Authority UI Clean-Up**: Simplified table ACTIONS column in the Licensing Authority portal to exclusively display `⚙️ Manage`, keeping secondary actions (`Reinstate`, `Delete`) inside the dedicated management modal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [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
|
## [v0.3.3] - 2026-09-27
|
||||||
|
|
||||||
### 🎨 Brand Identity, Centralized Asset Library & Design System
|
### 🎨 Brand Identity, Centralized Asset Library & Design System
|
||||||
|
|||||||
@@ -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.3
|
AppVersion=0.3.4.2
|
||||||
AppPublisher=LOGAR
|
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
|
||||||
@@ -17,9 +19,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
|
|||||||
WizardSmallImageFile=assets\wizard_small.bmp
|
WizardSmallImageFile=assets\wizard_small.bmp
|
||||||
VersionInfoCompany=LOGAR
|
VersionInfoCompany=LOGAR
|
||||||
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
|
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
|
||||||
VersionInfoVersion=0.3.3.0
|
VersionInfoVersion=0.3.4.2
|
||||||
VersionInfoProductName=LOGAR Client
|
VersionInfoProductName=LOGAR Client
|
||||||
VersionInfoProductVersion=0.3.3
|
VersionInfoProductVersion=0.3.4.2
|
||||||
|
|
||||||
[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;
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ OutFile "../dist/LOGAR-Client-Setup.exe"
|
|||||||
InstallDir "$PROGRAMFILES64\LOGAR"
|
InstallDir "$PROGRAMFILES64\LOGAR"
|
||||||
RequestExecutionLevel admin
|
RequestExecutionLevel admin
|
||||||
|
|
||||||
BrandingText "LOGAR Centralized Log Aggregator & Forwarder"
|
BrandingText "LOGAR Centralized Log Aggregator && Forwarder"
|
||||||
|
|
||||||
VIProductVersion "0.3.3.0"
|
VIProductVersion "0.3.4.2"
|
||||||
VIAddVersionKey "ProductName" "LOGAR Client"
|
VIAddVersionKey "ProductName" "LOGAR Client"
|
||||||
VIAddVersionKey "CompanyName" "LOGAR"
|
VIAddVersionKey "CompanyName" "LOGAR"
|
||||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
||||||
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
|
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
|
||||||
VIAddVersionKey "FileVersion" "0.3.3.0"
|
VIAddVersionKey "FileVersion" "0.3.4.2"
|
||||||
VIAddVersionKey "ProductVersion" "0.3.3.0"
|
VIAddVersionKey "ProductVersion" "0.3.4.2"
|
||||||
|
|
||||||
# --- Visual Graphics & Branding ---
|
# --- Visual Graphics & Branding ---
|
||||||
!define MUI_ICON "assets/logo.ico"
|
!define MUI_ICON "assets/logo.ico"
|
||||||
@@ -28,6 +28,7 @@ VIAddVersionKey "ProductVersion" "0.3.3.0"
|
|||||||
|
|
||||||
Var DATADIR
|
Var DATADIR
|
||||||
Var CFGFILE
|
Var CFGFILE
|
||||||
|
Var IS_UPDATE
|
||||||
Var CfgDialog
|
Var CfgDialog
|
||||||
Var CfgFileInput
|
Var CfgFileInput
|
||||||
Var CfgBrowseBtn
|
Var CfgBrowseBtn
|
||||||
@@ -43,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
|
||||||
|
|
||||||
@@ -51,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
|
||||||
|
|
||||||
@@ -71,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
|
||||||
@@ -86,13 +127,13 @@ Function ConfigPageCreate
|
|||||||
|
|
||||||
!insertmacro MUI_HEADER_TEXT "Import Client Configuration (Optional)" "Choose a client_config.json profile exported from LOGAR Server."
|
!insertmacro MUI_HEADER_TEXT "Import Client Configuration (Optional)" "Choose a client_config.json profile exported from LOGAR Server."
|
||||||
|
|
||||||
${NSD_CreateLabel} 0 0 100% 30u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
|
${NSD_CreateLabel} 0 0 100% 48u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
|
||||||
Pop $0
|
Pop $0
|
||||||
|
|
||||||
${NSD_CreateText} 0 38u 75% 12u "$CFGFILE"
|
${NSD_CreateText} 0 54u 75% 14u "$CFGFILE"
|
||||||
Pop $CfgFileInput
|
Pop $CfgFileInput
|
||||||
|
|
||||||
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
|
${NSD_CreateButton} 78% 53u 22% 16u "Browse..."
|
||||||
Pop $CfgBrowseBtn
|
Pop $CfgBrowseBtn
|
||||||
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
|
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
|
||||||
|
|
||||||
@@ -130,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}
|
||||||
@@ -145,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.3
|
AppVersion=0.3.4.2
|
||||||
AppPublisher=LOGAR
|
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
|
||||||
@@ -17,9 +19,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
|
|||||||
WizardSmallImageFile=assets\wizard_small.bmp
|
WizardSmallImageFile=assets\wizard_small.bmp
|
||||||
VersionInfoCompany=LOGAR
|
VersionInfoCompany=LOGAR
|
||||||
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
|
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
|
||||||
VersionInfoVersion=0.3.3.0
|
VersionInfoVersion=0.3.4.2
|
||||||
VersionInfoProductName=LOGAR Server
|
VersionInfoProductName=LOGAR Server
|
||||||
VersionInfoProductVersion=0.3.3
|
VersionInfoProductVersion=0.3.4.2
|
||||||
|
|
||||||
[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;
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ OutFile "../dist/LOGAR-Server-Setup.exe"
|
|||||||
InstallDir "$PROGRAMFILES64\LOGAR-Server"
|
InstallDir "$PROGRAMFILES64\LOGAR-Server"
|
||||||
RequestExecutionLevel admin
|
RequestExecutionLevel admin
|
||||||
|
|
||||||
BrandingText "LOGAR Centralized Log Aggregator & Ingestion Hub"
|
BrandingText "LOGAR Centralized Log Aggregator && Ingestion Hub"
|
||||||
|
|
||||||
VIProductVersion "0.3.3.0"
|
VIProductVersion "0.3.4.2"
|
||||||
VIAddVersionKey "ProductName" "LOGAR Server"
|
VIAddVersionKey "ProductName" "LOGAR Server"
|
||||||
VIAddVersionKey "CompanyName" "LOGAR"
|
VIAddVersionKey "CompanyName" "LOGAR"
|
||||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
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.3.0"
|
VIAddVersionKey "FileVersion" "0.3.4.2"
|
||||||
VIAddVersionKey "ProductVersion" "0.3.3.0"
|
VIAddVersionKey "ProductVersion" "0.3.4.2"
|
||||||
|
|
||||||
# --- Visual Graphics & Branding ---
|
# --- Visual Graphics & Branding ---
|
||||||
!define MUI_ICON "assets/logo.ico"
|
!define MUI_ICON "assets/logo.ico"
|
||||||
@@ -26,6 +26,7 @@ VIAddVersionKey "ProductVersion" "0.3.3.0"
|
|||||||
!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
|
||||||
@@ -38,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
|
||||||
|
|
||||||
@@ -46,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
|
||||||
|
|
||||||
@@ -63,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"
|
||||||
@@ -95,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
|
||||||
|
|
||||||
|
|||||||
+1728
-73
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
|
|||||||
OID_LOGAR_SEATS = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.1")
|
OID_LOGAR_SEATS = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.1")
|
||||||
OID_LOGAR_LICENSE_ID = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.2")
|
OID_LOGAR_LICENSE_ID = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.2")
|
||||||
OID_LOGAR_ISSUER_URL = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.3")
|
OID_LOGAR_ISSUER_URL = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.3")
|
||||||
|
OID_LOGAR_FEATURES = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.4")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -448,6 +449,19 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
|
|||||||
except (x509.ExtensionNotFound, Exception):
|
except (x509.ExtensionNotFound, Exception):
|
||||||
issuer_url = None
|
issuer_url = None
|
||||||
|
|
||||||
|
features = []
|
||||||
|
try:
|
||||||
|
ext_feat = cert.extensions.get_extension_for_oid(OID_LOGAR_FEATURES)
|
||||||
|
if ext_feat and ext_feat.value:
|
||||||
|
raw_val = ext_feat.value.value if hasattr(ext_feat.value, "value") else str(ext_feat.value)
|
||||||
|
feat_str = raw_val.decode("utf-8").strip() if isinstance(raw_val, bytes) else str(raw_val).strip()
|
||||||
|
if feat_str.startswith("["):
|
||||||
|
features = json.loads(feat_str)
|
||||||
|
else:
|
||||||
|
features = [f.strip() for f in feat_str.split(",") if f.strip()]
|
||||||
|
except Exception:
|
||||||
|
features = []
|
||||||
|
|
||||||
not_before = getattr(cert, "not_valid_before_utc", None) or cert.not_valid_before.replace(tzinfo=datetime.timezone.utc)
|
not_before = getattr(cert, "not_valid_before_utc", None) or cert.not_valid_before.replace(tzinfo=datetime.timezone.utc)
|
||||||
not_after = getattr(cert, "not_valid_after_utc", None) or cert.not_valid_after.replace(tzinfo=datetime.timezone.utc)
|
not_after = getattr(cert, "not_valid_after_utc", None) or cert.not_valid_after.replace(tzinfo=datetime.timezone.utc)
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
@@ -482,6 +496,7 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
|
|||||||
"seats": seats,
|
"seats": seats,
|
||||||
"license_id": license_id,
|
"license_id": license_id,
|
||||||
"issuer_url": issuer_url,
|
"issuer_url": issuer_url,
|
||||||
|
"features": features,
|
||||||
"subject_cn": subject_cn,
|
"subject_cn": subject_cn,
|
||||||
"issuer_cn": issuer_cn,
|
"issuer_cn": issuer_cn,
|
||||||
"is_ca": is_ca,
|
"is_ca": is_ca,
|
||||||
@@ -603,11 +618,12 @@ def sign_ca_csr_with_seats(
|
|||||||
seats: int,
|
seats: int,
|
||||||
days_valid: int = 365,
|
days_valid: int = 365,
|
||||||
license_id: Optional[str] = None,
|
license_id: Optional[str] = None,
|
||||||
license_server_url: str = "https://logar.eibl.tech"
|
license_server_url: str = "https://logar.eibl.tech",
|
||||||
|
features: Optional[List[str]] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Signs a subordinate CA CSR using the master license authority Root CA.
|
Signs a subordinate CA CSR using the master license authority Root CA.
|
||||||
Embeds the approved seat count and license ID via custom X.509 extensions.
|
Embeds the approved seat count, license ID, and optional enterprise features via custom X.509 extensions.
|
||||||
Returns the signed PEM certificate.
|
Returns the signed PEM certificate.
|
||||||
"""
|
"""
|
||||||
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
|
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
|
||||||
@@ -666,6 +682,13 @@ def sign_ca_csr_with_seats(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if features:
|
||||||
|
feat_str = json.dumps(features) if isinstance(features, list) else str(features)
|
||||||
|
builder = builder.add_extension(
|
||||||
|
x509.UnrecognizedExtension(OID_LOGAR_FEATURES, feat_str.encode("utf-8")),
|
||||||
|
critical=False
|
||||||
|
)
|
||||||
|
|
||||||
signed_cert = builder.sign(master_ca_key, hashes.SHA256())
|
signed_cert = builder.sign(master_ca_key, hashes.SHA256())
|
||||||
return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||||
|
|
||||||
@@ -903,6 +926,7 @@ def import_license_package(
|
|||||||
"master_root_ca_pem": master_root_ca_pem,
|
"master_root_ca_pem": master_root_ca_pem,
|
||||||
"license_info": info,
|
"license_info": info,
|
||||||
"seats": info.get("seats"),
|
"seats": info.get("seats"),
|
||||||
|
"features": info.get("features", []),
|
||||||
"license_id": info.get("license_id"),
|
"license_id": info.get("license_id"),
|
||||||
"issuer_url": info.get("issuer_url"),
|
"issuer_url": info.get("issuer_url"),
|
||||||
"fingerprint": info.get("fingerprint"),
|
"fingerprint": info.get("fingerprint"),
|
||||||
|
|||||||
@@ -470,8 +470,282 @@ 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"])
|
||||||
|
|
||||||
|
def test_persistent_session_secret_and_rate_limiting(self):
|
||||||
|
"""Tests DB persistence of session secret and sliding window rate limiting."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
|
||||||
|
# 1. Session secret persistence
|
||||||
|
sec1 = Server.get_session_secret(self.test_db)
|
||||||
|
self.assertTrue(len(sec1) >= 32)
|
||||||
|
conn = sqlite3.connect(self.test_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT value FROM server_meta WHERE key = 'session_secret'")
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
self.assertIsNotNone(row)
|
||||||
|
self.assertEqual(row[0], sec1)
|
||||||
|
|
||||||
|
# Token generated with test_db
|
||||||
|
token = Server.generate_session_token("admin", db_path=self.test_db)
|
||||||
|
self.assertEqual(Server.verify_session_token(token, db_path=self.test_db), "admin")
|
||||||
|
|
||||||
|
# 2. Rate limiting
|
||||||
|
test_ip = "10.0.0.99"
|
||||||
|
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
|
||||||
|
|
||||||
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||||
|
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
||||||
|
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
||||||
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||||
|
|
||||||
|
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
||||||
|
self.assertFalse(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||||
|
|
||||||
|
# Different scope ('enroll') should still be allowed
|
||||||
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="enroll", db_path=self.test_db))
|
||||||
|
|
||||||
|
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
|
||||||
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||||
|
|
||||||
|
def test_ingest_queue_manager_and_batch_flush(self):
|
||||||
|
"""Tests IngestQueueManager buffering, capacity bounds, and atomic batch flushing."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
qm = Server.IngestQueueManager(flush_interval=1.0, batch_size=5, max_queue=50)
|
||||||
|
|
||||||
|
# Initial stats
|
||||||
|
stats = qm.get_stats()
|
||||||
|
self.assertEqual(stats["queue_size"], 0)
|
||||||
|
self.assertEqual(stats["total_ingested"], 0)
|
||||||
|
self.assertEqual(stats["total_flushed"], 0)
|
||||||
|
|
||||||
|
# Push 3 payloads
|
||||||
|
for i in range(3):
|
||||||
|
payload = {
|
||||||
|
"server": f"node-{i}.corp.local",
|
||||||
|
"logs": [
|
||||||
|
{"server": f"node-{i}", "signature": f"Sig_{i}", "severity": "WARNING", "message": f"Log msg {i}", "os_type": "linux"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
pushed = qm.push(payload)
|
||||||
|
self.assertTrue(pushed)
|
||||||
|
|
||||||
|
stats = qm.get_stats()
|
||||||
|
self.assertEqual(stats["queue_size"], 3)
|
||||||
|
self.assertEqual(stats["total_ingested"], 3)
|
||||||
|
|
||||||
|
# Flush to database
|
||||||
|
res_flush = qm.flush(db_path=self.test_db, window_hours=12, min_runs=1)
|
||||||
|
self.assertEqual(res_flush["flushed_runs"], 3)
|
||||||
|
self.assertEqual(res_flush["processed_logs"], 3)
|
||||||
|
|
||||||
|
# Verify DB records
|
||||||
|
conn = sqlite3.connect(self.test_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT COUNT(*) FROM ingest_runs")
|
||||||
|
run_count = c.fetchone()[0]
|
||||||
|
c.execute("SELECT COUNT(*) FROM active_issues WHERE status = 'VERIFIED'")
|
||||||
|
issue_count = c.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
self.assertEqual(run_count, 3)
|
||||||
|
self.assertEqual(issue_count, 3)
|
||||||
|
|
||||||
|
# Stats after flush
|
||||||
|
stats = qm.get_stats()
|
||||||
|
self.assertEqual(stats["queue_size"], 0)
|
||||||
|
self.assertEqual(stats["total_flushed"], 3)
|
||||||
|
|
||||||
|
def test_enterprise_db_feature_gating(self):
|
||||||
|
"""Tests feature check helpers and DB storage info under standard and enterprise licensed states."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
|
||||||
|
# 1. Standard / Unlicensed state
|
||||||
|
self.assertFalse(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
||||||
|
info = Server.get_storage_engine_info(db_path=self.test_db)
|
||||||
|
self.assertEqual(info["engine"], "sqlite")
|
||||||
|
self.assertFalse(info["feature_unlocked"])
|
||||||
|
self.assertIn("SQLite 3 (WAL Mode)", info["backend_display"])
|
||||||
|
|
||||||
|
# 2. Unlock enterprise_db feature in DB
|
||||||
|
conn = sqlite3.connect(self.test_db)
|
||||||
|
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db", "ai_heuristics"]),))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
self.assertTrue(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
||||||
|
self.assertTrue(Server.is_feature_licensed("ai_heuristics", db_path=self.test_db))
|
||||||
|
self.assertFalse(Server.is_feature_licensed("unknown_feat", db_path=self.test_db))
|
||||||
|
|
||||||
|
info_unlocked = Server.get_storage_engine_info(db_path=self.test_db)
|
||||||
|
self.assertTrue(info_unlocked["feature_unlocked"])
|
||||||
|
self.assertIn("enterprise_db", info_unlocked["licensed_features"])
|
||||||
|
|
||||||
|
def test_postgres_storage_endpoints_authorization(self):
|
||||||
|
"""Tests REST API security gating on DB engine management endpoints."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "storage_engine": "sqlite"}
|
||||||
|
|
||||||
|
# 1. Test direct authorization & endpoint logic
|
||||||
|
stats = Server.api_db_stats()
|
||||||
|
self.assertIn("queue", stats)
|
||||||
|
self.assertIn("storage", stats)
|
||||||
|
self.assertFalse(stats["enterprise_db_licensed"])
|
||||||
|
|
||||||
|
# 2. Test Connection without enterprise_db -> HTTPException 403
|
||||||
|
pg_req = Server.DbConnectionTestRequest(host="localhost", port=5432, database="logar", user="postgres", password="")
|
||||||
|
with self.assertRaises(Server.HTTPException) as cm:
|
||||||
|
Server.api_db_test_connection(pg_req)
|
||||||
|
self.assertEqual(cm.exception.status_code, 403)
|
||||||
|
self.assertIn("enterprise_db", cm.exception.detail)
|
||||||
|
|
||||||
|
# 3. Switch backend without enterprise_db -> HTTPException 403
|
||||||
|
switch_req = Server.DbSwitchBackendRequest(db_type="postgres", host="localhost", port=5432, database="logar", user="postgres", password="")
|
||||||
|
with self.assertRaises(Server.HTTPException) as cm:
|
||||||
|
Server.api_db_switch_backend(switch_req)
|
||||||
|
self.assertEqual(cm.exception.status_code, 403)
|
||||||
|
self.assertIn("enterprise_db", cm.exception.detail)
|
||||||
|
|
||||||
|
# 4. Flush buffer endpoint executes successfully
|
||||||
|
flush_res = Server.api_db_flush_buffer()
|
||||||
|
self.assertEqual(flush_res["status"], "success")
|
||||||
|
|
||||||
|
# 5. Optional HTTP client verification if starlette testclient dependencies are present
|
||||||
|
try:
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
client = TestClient(Server.app)
|
||||||
|
|
||||||
|
# Unauthenticated request to /api/db/stats -> 401
|
||||||
|
res = client.get("/api/db/stats")
|
||||||
|
self.assertEqual(res.status_code, 401)
|
||||||
|
|
||||||
|
# Authenticated request with session cookie
|
||||||
|
token = Server.generate_session_token("admin", db_path=self.test_db)
|
||||||
|
client.cookies.set(Server.SESSION_COOKIE_NAME, token)
|
||||||
|
|
||||||
|
res_auth = client.get("/api/db/stats")
|
||||||
|
self.assertEqual(res_auth.status_code, 200)
|
||||||
|
data = res_auth.json()
|
||||||
|
self.assertIn("queue", data)
|
||||||
|
self.assertIn("storage", data)
|
||||||
|
self.assertFalse(data["enterprise_db_licensed"])
|
||||||
|
|
||||||
|
# 403 checks via HTTP client
|
||||||
|
res_test = client.post("/api/db/test-connection", json={"host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
|
||||||
|
self.assertEqual(res_test.status_code, 403)
|
||||||
|
|
||||||
|
res_switch = client.post("/api/db/switch-backend", json={"db_type": "postgres", "host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
|
||||||
|
self.assertEqual(res_switch.status_code, 403)
|
||||||
|
|
||||||
|
res_flush = client.post("/api/db/flush-buffer")
|
||||||
|
self.assertEqual(res_flush.status_code, 200)
|
||||||
|
except (ImportError, RuntimeError, Exception):
|
||||||
|
# Starlette TestClient may raise RuntimeError when httpx/httpx2 is missing in lightweight runner environments
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_db_encryption_at_rest_and_full_config_api(self):
|
||||||
|
"""Tests OpenPGP data-at-rest encryption/decryption and full DB/Cache configuration API."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
priv_armored, pub_armored, fp = Server.generate_server_keypair("test-db-node")
|
||||||
|
priv_key, _ = pgpy.PGPKey.from_blob(priv_armored)
|
||||||
|
pub_key, _ = pgpy.PGPKey.from_blob(pub_armored)
|
||||||
|
|
||||||
|
Server.SERVER_STATE["config"] = {
|
||||||
|
"db_path": self.test_db,
|
||||||
|
"storage_engine": "sqlite",
|
||||||
|
"cache_mode": "direct",
|
||||||
|
"encrypt_at_rest": False,
|
||||||
|
"private_key": priv_armored,
|
||||||
|
"public_key": pub_armored,
|
||||||
|
"server_fingerprint": fp
|
||||||
|
}
|
||||||
|
Server.SERVER_STATE["private_key_obj"] = priv_key
|
||||||
|
Server.SERVER_STATE["public_key_obj"] = pub_key
|
||||||
|
|
||||||
|
# 1. Test OpenPGP encryption & decryption roundtrip
|
||||||
|
raw_msg = "Critical database corruption: segment 0x89FA damaged"
|
||||||
|
encrypted_msg = Server.encrypt_field_for_storage(raw_msg, force=True)
|
||||||
|
self.assertTrue(encrypted_msg.startswith("-----BEGIN PGP MESSAGE-----"))
|
||||||
|
self.assertNotIn("Critical database corruption", encrypted_msg)
|
||||||
|
|
||||||
|
decrypted_msg = Server.decrypt_field_from_storage(encrypted_msg)
|
||||||
|
self.assertEqual(decrypted_msg, raw_msg)
|
||||||
|
|
||||||
|
# 2. Test updating config via api_db_update_config (direct -> buffered, encrypt_at_rest = True)
|
||||||
|
cfg_req = Server.DbFullConfigRequest(
|
||||||
|
storage_engine="sqlite",
|
||||||
|
cache_mode="buffered",
|
||||||
|
flush_interval=4.0,
|
||||||
|
batch_size=200,
|
||||||
|
max_queue=25000,
|
||||||
|
encrypt_at_rest=True
|
||||||
|
)
|
||||||
|
res = Server.api_db_update_config(cfg_req)
|
||||||
|
self.assertEqual(res["status"], "success")
|
||||||
|
self.assertEqual(Server.SERVER_STATE["config"]["cache_mode"], "buffered")
|
||||||
|
self.assertTrue(Server.SERVER_STATE["config"]["encrypt_at_rest"])
|
||||||
|
self.assertEqual(Server.INGEST_QUEUE.flush_interval, 4.0)
|
||||||
|
self.assertEqual(Server.INGEST_QUEUE.batch_size, 200)
|
||||||
|
|
||||||
|
# 3. Test that switching to external PostgreSQL requires enterprise_db license
|
||||||
|
pg_req = Server.DbFullConfigRequest(
|
||||||
|
storage_engine="postgres",
|
||||||
|
cache_mode="direct",
|
||||||
|
pg_config=Server.DbConnectionTestRequest(host="db.corp.internal", port=5432, database="logar", user="postgres", password="")
|
||||||
|
)
|
||||||
|
with self.assertRaises(Server.HTTPException) as cm:
|
||||||
|
Server.api_db_update_config(pg_req)
|
||||||
|
self.assertEqual(cm.exception.status_code, 403)
|
||||||
|
self.assertIn("enterprise_db", cm.exception.detail)
|
||||||
|
|
||||||
|
# 4. Verify storage engine info output
|
||||||
|
info = Server.get_storage_engine_info(db_path=self.test_db)
|
||||||
|
self.assertEqual(info["cache_mode"], "buffered")
|
||||||
|
self.assertTrue(info["encrypt_at_rest"])
|
||||||
|
self.assertEqual(info["flush_interval"], 4.0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user