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
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
client_state.json
|
||||
*.tmp
|
||||
|
||||
# Live configuration with generated private keys & tokens (samples are tracked)
|
||||
server_config.json
|
||||
client_config.json
|
||||
client_state.json
|
||||
local_collector_state.json
|
||||
test_*.json
|
||||
test_*.db
|
||||
test_pipeline_master_ca/
|
||||
test_pipeline_certs/
|
||||
master_ca_certs/
|
||||
.env
|
||||
*.env
|
||||
|
||||
+1
-1
Submodule LOGAR-LICENSING updated: 119061721a...38a41cfbac
@@ -1,14 +1,13 @@
|
||||
<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>
|
||||
|
||||
# LOGAR: Fleet Log Aggregation & Centralized Provisioning System
|
||||
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
|
||||
|
||||
[](RELEASE_NOTES.md)
|
||||
[](tests/)
|
||||
[](RELEASE_NOTES.md)
|
||||
[](tests/)
|
||||
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||
|
||||
**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)**:
|
||||
|
||||
* [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)
|
||||
* [System State Machines & Subsystem Lifecycles](https://gitea.eibl.tech/me0nline/LOGAR/wiki/State-Machines-and-Lifecycles)
|
||||
* [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
|
||||
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
|
||||
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
|
||||
* [Branding & Design System Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# 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
|
||||
|
||||
### 🎨 Brand Identity, Centralized Asset Library & Design System
|
||||
|
||||
@@ -7,6 +7,26 @@ ENROLL_SECRET="${2:-}"
|
||||
INSTALL_DIR="/opt/logar-client"
|
||||
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..."
|
||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
|
||||
chmod 700 "${CONFIG_DIR}/certs"
|
||||
|
||||
@@ -21,14 +21,34 @@ echo "============================================================"
|
||||
echo " LOGAR Server Enterprise Linux Installer"
|
||||
echo "============================================================"
|
||||
|
||||
# Interactive configuration if running in a terminal without CLI arguments
|
||||
if [ -t 0 ]; then
|
||||
# 1. Mutual Exclusion Check: Deny installation if LOGAR Client is present
|
||||
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
|
||||
if [ -n "${USER_CFG}" ]; then
|
||||
CONFIG_DIR="${USER_CFG}"
|
||||
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"
|
||||
|
||||
if [ -f "dist/Server.bin" ]; then
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
[Setup]
|
||||
AppId={{9C4A6F1D-8975-476E-8512-4217DE029A88}}
|
||||
AppName=LOGAR Client
|
||||
AppVersion=0.3.3
|
||||
AppVersion=0.3.4.2
|
||||
AppPublisher=LOGAR
|
||||
AppPublisherURL=https://logar.eibl.tech
|
||||
AppSupportURL=https://logar.eibl.tech
|
||||
AppUpdatesURL=https://logar.eibl.tech
|
||||
DefaultDirName={autopf}\LOGAR
|
||||
UsePreviousAppDir=yes
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=LOGAR-Client-Setup
|
||||
PrivilegesRequired=admin
|
||||
@@ -17,9 +19,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
|
||||
WizardSmallImageFile=assets\wizard_small.bmp
|
||||
VersionInfoCompany=LOGAR
|
||||
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
|
||||
VersionInfoVersion=0.3.3.0
|
||||
VersionInfoVersion=0.3.4.2
|
||||
VersionInfoProductName=LOGAR Client
|
||||
VersionInfoProductVersion=0.3.3
|
||||
VersionInfoProductVersion=0.3.4.2
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
@@ -54,6 +56,58 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR"
|
||||
var
|
||||
DataDirPage: TInputDirWizardPage;
|
||||
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;
|
||||
begin
|
||||
@@ -67,6 +121,9 @@ begin
|
||||
''
|
||||
);
|
||||
DataDirPage.Add('Data Directory:');
|
||||
if (ExistingDataDir <> '') then
|
||||
DataDirPage.Values[0] := ExistingDataDir
|
||||
else
|
||||
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
|
||||
|
||||
ConfigFilePage := CreateInputFilePage(
|
||||
@@ -80,8 +137,23 @@ begin
|
||||
ConfigFilePage.Add('Client Configuration (client_config.json):', 'JSON Files (*.json)|*.json|All Files (*.*)|*.*', '.json');
|
||||
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;
|
||||
begin
|
||||
if IsUpdateMode and (ExistingDataDir <> '') then
|
||||
begin
|
||||
Result := ExistingDataDir;
|
||||
Exit;
|
||||
end;
|
||||
Result := DataDirPage.Values[0];
|
||||
if Result = '' then
|
||||
Result := ExpandConstant('{commonappdata}\LOGAR');
|
||||
@@ -107,8 +179,11 @@ begin
|
||||
end
|
||||
else if CurStep = ssPostInstall then
|
||||
begin
|
||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'InstallDir', ExpandConstant('{app}'));
|
||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
|
||||
|
||||
if not IsUpdateMode then
|
||||
begin
|
||||
CfgPath := GetConfigFile('');
|
||||
if (CfgPath <> '') and FileExists(CfgPath) then
|
||||
begin
|
||||
@@ -117,6 +192,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
var
|
||||
|
||||
@@ -7,15 +7,15 @@ OutFile "../dist/LOGAR-Client-Setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\LOGAR"
|
||||
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 "CompanyName" "LOGAR"
|
||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
||||
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
|
||||
VIAddVersionKey "FileVersion" "0.3.3.0"
|
||||
VIAddVersionKey "ProductVersion" "0.3.3.0"
|
||||
VIAddVersionKey "FileVersion" "0.3.4.2"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.2"
|
||||
|
||||
# --- Visual Graphics & Branding ---
|
||||
!define MUI_ICON "assets/logo.ico"
|
||||
@@ -28,6 +28,7 @@ VIAddVersionKey "ProductVersion" "0.3.3.0"
|
||||
|
||||
Var DATADIR
|
||||
Var CFGFILE
|
||||
Var IS_UPDATE
|
||||
Var CfgDialog
|
||||
Var CfgFileInput
|
||||
Var CfgBrowseBtn
|
||||
@@ -43,6 +44,7 @@ Var CfgBrowseBtn
|
||||
# Directory Page 1: Installation Directory
|
||||
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
||||
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
||||
!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_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_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
@@ -71,13 +74,51 @@ Page custom ConfigPageCreate ConfigPageLeave
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
Function SkipIfUpdate
|
||||
${If} $IS_UPDATE == "1"
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function .onInit
|
||||
SetShellVarContext all
|
||||
StrCpy $DATADIR "$APPDATA\LOGAR"
|
||||
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
|
||||
|
||||
Function ConfigPageCreate
|
||||
${If} $IS_UPDATE == "1"
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
nsDialogs::Create 1018
|
||||
Pop $CfgDialog
|
||||
${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."
|
||||
|
||||
${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
|
||||
|
||||
${NSD_CreateText} 0 38u 75% 12u "$CFGFILE"
|
||||
${NSD_CreateText} 0 54u 75% 14u "$CFGFILE"
|
||||
Pop $CfgFileInput
|
||||
|
||||
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
|
||||
${NSD_CreateButton} 78% 53u 22% 16u "Browse..."
|
||||
Pop $CfgBrowseBtn
|
||||
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
|
||||
|
||||
@@ -130,8 +171,9 @@ Section "Install"
|
||||
CreateDirectory "$DATADIR"
|
||||
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
||||
|
||||
# If client config file was selected, copy to data directory
|
||||
${If} $CFGFILE != ""
|
||||
# If client config file was selected and not updating existing config, copy to data directory
|
||||
${If} $IS_UPDATE != "1"
|
||||
${AndIf} $CFGFILE != ""
|
||||
${AndIf} ${FileExists} "$CFGFILE"
|
||||
CopyFiles /SILENT "$CFGFILE" "$DATADIR\client_config.json"
|
||||
${EndIf}
|
||||
@@ -145,6 +187,7 @@ Section "Install"
|
||||
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "InstallDir" "$INSTDIR"
|
||||
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "DataDir" "$DATADIR"
|
||||
SectionEnd
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
[Setup]
|
||||
AppId={{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}
|
||||
AppName=LOGAR Server
|
||||
AppVersion=0.3.3
|
||||
AppVersion=0.3.4.2
|
||||
AppPublisher=LOGAR
|
||||
AppPublisherURL=https://logar.eibl.tech
|
||||
AppSupportURL=https://logar.eibl.tech
|
||||
AppUpdatesURL=https://logar.eibl.tech
|
||||
DefaultDirName={autopf}\LOGAR-Server
|
||||
UsePreviousAppDir=yes
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=LOGAR-Server-Setup
|
||||
PrivilegesRequired=admin
|
||||
@@ -17,9 +19,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
|
||||
WizardSmallImageFile=assets\wizard_small.bmp
|
||||
VersionInfoCompany=LOGAR
|
||||
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
|
||||
VersionInfoVersion=0.3.3.0
|
||||
VersionInfoVersion=0.3.4.2
|
||||
VersionInfoProductName=LOGAR Server
|
||||
VersionInfoProductVersion=0.3.3
|
||||
VersionInfoProductVersion=0.3.4.2
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
@@ -54,6 +56,58 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server"
|
||||
[Code]
|
||||
var
|
||||
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;
|
||||
begin
|
||||
@@ -67,11 +121,29 @@ begin
|
||||
''
|
||||
);
|
||||
DataDirPage.Add('Data Directory:');
|
||||
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;
|
||||
|
||||
function GetDataDir(Param: String): String;
|
||||
begin
|
||||
if IsUpdateMode and (ExistingDataDir <> '') then
|
||||
begin
|
||||
Result := ExistingDataDir;
|
||||
Exit;
|
||||
end;
|
||||
Result := DataDirPage.Values[0];
|
||||
if Result = '' then
|
||||
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||
@@ -91,6 +163,7 @@ begin
|
||||
end
|
||||
else if CurStep = ssPostInstall then
|
||||
begin
|
||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'InstallDir', ExpandConstant('{app}'));
|
||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -6,15 +6,15 @@ OutFile "../dist/LOGAR-Server-Setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\LOGAR-Server"
|
||||
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 "CompanyName" "LOGAR"
|
||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
||||
VIAddVersionKey "FileDescription" "LOGAR Cloud Hub & TCP Ingestion Server Setup"
|
||||
VIAddVersionKey "FileVersion" "0.3.3.0"
|
||||
VIAddVersionKey "ProductVersion" "0.3.3.0"
|
||||
VIAddVersionKey "FileVersion" "0.3.4.2"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.2"
|
||||
|
||||
# --- Visual Graphics & Branding ---
|
||||
!define MUI_ICON "assets/logo.ico"
|
||||
@@ -26,6 +26,7 @@ VIAddVersionKey "ProductVersion" "0.3.3.0"
|
||||
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets/wizard_sidebar.bmp"
|
||||
|
||||
Var DATADIR
|
||||
Var IS_UPDATE
|
||||
|
||||
# --- UI Pages ---
|
||||
!define MUI_ABORTWARNING
|
||||
@@ -38,6 +39,7 @@ Var DATADIR
|
||||
# Directory Page 1: Installation Directory
|
||||
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
||||
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
||||
!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_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_PAGE_CUSTOMFUNCTION_PRE SkipIfUpdate
|
||||
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
@@ -63,9 +66,43 @@ Var DATADIR
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
Function SkipIfUpdate
|
||||
${If} $IS_UPDATE == "1"
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function .onInit
|
||||
SetShellVarContext all
|
||||
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
|
||||
|
||||
Section "Install"
|
||||
@@ -95,6 +132,7 @@ Section "Install"
|
||||
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "InstallDir" "$INSTDIR"
|
||||
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "DataDir" "$DATADIR"
|
||||
SectionEnd
|
||||
|
||||
|
||||
+1714
-59
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
OID_LOGAR_SEATS = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.1")
|
||||
OID_LOGAR_LICENSE_ID = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.2")
|
||||
OID_LOGAR_ISSUER_URL = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.3")
|
||||
OID_LOGAR_FEATURES = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.4")
|
||||
|
||||
|
||||
|
||||
@@ -448,6 +449,19 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
|
||||
except (x509.ExtensionNotFound, Exception):
|
||||
issuer_url = None
|
||||
|
||||
features = []
|
||||
try:
|
||||
ext_feat = cert.extensions.get_extension_for_oid(OID_LOGAR_FEATURES)
|
||||
if ext_feat and ext_feat.value:
|
||||
raw_val = ext_feat.value.value if hasattr(ext_feat.value, "value") else str(ext_feat.value)
|
||||
feat_str = raw_val.decode("utf-8").strip() if isinstance(raw_val, bytes) else str(raw_val).strip()
|
||||
if feat_str.startswith("["):
|
||||
features = json.loads(feat_str)
|
||||
else:
|
||||
features = [f.strip() for f in feat_str.split(",") if f.strip()]
|
||||
except Exception:
|
||||
features = []
|
||||
|
||||
not_before = getattr(cert, "not_valid_before_utc", None) or cert.not_valid_before.replace(tzinfo=datetime.timezone.utc)
|
||||
not_after = getattr(cert, "not_valid_after_utc", None) or cert.not_valid_after.replace(tzinfo=datetime.timezone.utc)
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
@@ -482,6 +496,7 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
|
||||
"seats": seats,
|
||||
"license_id": license_id,
|
||||
"issuer_url": issuer_url,
|
||||
"features": features,
|
||||
"subject_cn": subject_cn,
|
||||
"issuer_cn": issuer_cn,
|
||||
"is_ca": is_ca,
|
||||
@@ -603,11 +618,12 @@ def sign_ca_csr_with_seats(
|
||||
seats: int,
|
||||
days_valid: int = 365,
|
||||
license_id: Optional[str] = None,
|
||||
license_server_url: str = "https://logar.eibl.tech"
|
||||
license_server_url: str = "https://logar.eibl.tech",
|
||||
features: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
Signs a subordinate CA CSR using the master license authority Root CA.
|
||||
Embeds the approved seat count and license ID via custom X.509 extensions.
|
||||
Embeds the approved seat count, license ID, and optional enterprise features via custom X.509 extensions.
|
||||
Returns the signed PEM certificate.
|
||||
"""
|
||||
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
|
||||
@@ -666,6 +682,13 @@ def sign_ca_csr_with_seats(
|
||||
)
|
||||
)
|
||||
|
||||
if features:
|
||||
feat_str = json.dumps(features) if isinstance(features, list) else str(features)
|
||||
builder = builder.add_extension(
|
||||
x509.UnrecognizedExtension(OID_LOGAR_FEATURES, feat_str.encode("utf-8")),
|
||||
critical=False
|
||||
)
|
||||
|
||||
signed_cert = builder.sign(master_ca_key, hashes.SHA256())
|
||||
return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||
|
||||
@@ -903,6 +926,7 @@ def import_license_package(
|
||||
"master_root_ca_pem": master_root_ca_pem,
|
||||
"license_info": info,
|
||||
"seats": info.get("seats"),
|
||||
"features": info.get("features", []),
|
||||
"license_id": info.get("license_id"),
|
||||
"issuer_url": info.get("issuer_url"),
|
||||
"fingerprint": info.get("fingerprint"),
|
||||
|
||||
@@ -470,8 +470,282 @@ class TestServerComponent(unittest.TestCase):
|
||||
self.assertEqual(client_data["active_seats"], 0)
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user