Compare commits
77
Commits
27de5e9e06
..
v0.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37f959e402 | ||
|
|
f1e5e8f986 | ||
|
|
5a0fdb4374 | ||
|
|
fdfa8dfe7f | ||
|
|
b40966ac4c | ||
|
|
e47fe609a6 | ||
|
|
48bb279601 | ||
|
|
fd34fe9911 | ||
|
|
a7ff1c845d | ||
|
|
b0066c9ea6 | ||
|
|
079c6cb70f | ||
|
|
49525594a3 | ||
|
|
04ccc999a8 | ||
|
|
267aa54076 | ||
|
|
2c9f8d9b91 | ||
|
|
125870b135 | ||
|
|
f0df5a6e4d | ||
|
|
5b59b18c2c | ||
|
|
ec80908d5b | ||
|
|
92d858841d | ||
|
|
0e616d614c | ||
|
|
38b36c28a0 | ||
|
|
c51194e699 | ||
|
|
3a3d251817 | ||
|
|
25adb46d48 | ||
|
|
b33294f36c | ||
|
|
b8069ee66b | ||
|
|
d025689193 | ||
|
|
96126cdf84 | ||
|
|
17c1791b7d | ||
|
|
911014fae7 | ||
|
|
de95a3283d | ||
|
|
b0bf322275 | ||
|
|
a496b7e822 | ||
|
|
29784e166f | ||
|
|
56844983e2 | ||
|
|
07b8f48631 | ||
|
|
fa8b9e0af5 | ||
|
|
93d9f0e463 | ||
|
|
41cb956416 | ||
|
|
42916dc8ee | ||
|
|
a34934959d | ||
|
|
fe296b42e7 | ||
|
|
e04082d4b8 | ||
|
|
ad022cc447 | ||
|
|
344d57ff49 | ||
|
|
19c60616b1 | ||
|
|
14b3586b10 | ||
|
|
a9d5d641cd | ||
|
|
1d8449515e | ||
|
|
668f6e66cd | ||
|
|
6ac2c89aac | ||
|
|
cce439f0e2 | ||
|
|
2c6265b6cc | ||
|
|
4396052ed3 | ||
|
|
c2fed0379c | ||
|
|
e01421ea8c | ||
|
|
2e10aa4819 | ||
|
|
777d6300a8 | ||
|
|
c3bb440eff | ||
|
|
ea3a2cd541 | ||
|
|
8c6d3f57f9 | ||
|
|
f7584320f9 | ||
|
|
d9d895e468 | ||
|
|
4c8db1ec94 | ||
|
|
5fe74325ec | ||
|
|
871d3c15ab | ||
|
|
7c57cf0645 | ||
|
|
2d75b595e6 | ||
|
|
d152b65f6e | ||
|
|
eced9bc5a1 | ||
|
|
3292702dd3 | ||
|
|
fac6ae8847 | ||
|
|
f4996a53cc | ||
|
|
05f703b024 | ||
|
|
eb628ff40b | ||
|
|
79a2720363 |
@@ -0,0 +1,58 @@
|
|||||||
|
name: Build and Push LOGAR Runner
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
name: Build & Push Runner Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Ensure Docker CLI is available
|
||||||
|
run: |
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "Docker CLI not found. Downloading static Docker CLI binary..."
|
||||||
|
curl -fSL --connect-timeout 15 --max-time 120 --retry 3 https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz -o /tmp/docker.tgz
|
||||||
|
tar -xzf /tmp/docker.tgz -C /usr/local/bin --strip-components=1 docker/docker
|
||||||
|
chmod +x /usr/local/bin/docker
|
||||||
|
rm -f /tmp/docker.tgz
|
||||||
|
fi
|
||||||
|
docker version
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
run: |
|
||||||
|
REGISTRY="gitea.eibl.tech"
|
||||||
|
USER="${{ secrets.GITEAUSER || secrets.PACKAGESUSER || github.actor }}"
|
||||||
|
PASS="${{ secrets.GITEATOKEN || secrets.PACKAGESTOKEN || secrets.TAG_TOKEN || github.token }}"
|
||||||
|
echo "Logging in to ${REGISTRY} as ${USER}..."
|
||||||
|
echo "${PASS}" | docker login "${REGISTRY}" -u "${USER}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: "0"
|
||||||
|
run: |
|
||||||
|
IMAGE_TAG="gitea.eibl.tech/me0nline/logar-runner:latest"
|
||||||
|
echo "Building ${IMAGE_TAG}..."
|
||||||
|
docker build \
|
||||||
|
-t "${IMAGE_TAG}" \
|
||||||
|
-f runner/Dockerfile \
|
||||||
|
runner/
|
||||||
|
|
||||||
|
- name: Push to Gitea Container Registry
|
||||||
|
run: |
|
||||||
|
IMAGE_TAG="gitea.eibl.tech/me0nline/logar-runner:latest"
|
||||||
|
echo "Pushing ${IMAGE_TAG} to Gitea Container Registry..."
|
||||||
|
docker push "${IMAGE_TAG}"
|
||||||
|
echo "Successfully pushed ${IMAGE_TAG} to Gitea Container Registry!"
|
||||||
|
|
||||||
|
- name: Trigger Portainer redeploy via webhook
|
||||||
|
run: |
|
||||||
|
echo "Notifying Portainer to redeploy the runner stack..."
|
||||||
|
curl -s -X POST \
|
||||||
|
"https://portainer.eibl.tech/api/stacks/webhooks/6e67c0b7-1c5d-4bb2-b8b8-4a6ec66d3b87"
|
||||||
|
echo "✓ Portainer webhook triggered"
|
||||||
+11
-12
@@ -15,19 +15,18 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Run Component Tests & Pipeline Verification
|
name: Run Component Tests & Pipeline Verification
|
||||||
if: "!startsWith(github.ref, 'refs/tags/')"
|
if: "!startsWith(github.ref, 'refs/tags/')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: logar
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install System Dependencies & Python
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
python3 compilation/ensure_pip.py
|
||||||
apt-get update -y
|
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
|
||||||
apt-get install -y python3 python3-pip python3-venv curl
|
python3 -m pip install -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install -r compilation/requirements.txt
|
||||||
fi
|
|
||||||
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
|
||||||
pip3 install -r compilation/requirements.txt --break-system-packages || pip3 install -r compilation/requirements.txt
|
|
||||||
|
|
||||||
- name: Verify Python Syntax
|
- name: Verify Python Syntax
|
||||||
run: |
|
run: |
|
||||||
@@ -35,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Component Unit Tests
|
- name: Run Component Unit Tests
|
||||||
run: |
|
run: |
|
||||||
python3 -m unittest discover -s tests -v
|
PYTHONWARNINGS="ignore" python3 -m unittest discover -s tests -v
|
||||||
|
|
||||||
- name: Run End-to-End Pipeline Integration Test
|
- name: Run End-to-End Pipeline Integration Test
|
||||||
run: |
|
run: |
|
||||||
@@ -50,10 +49,10 @@ jobs:
|
|||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
echo "[*] Server launched in background with PID $SERVER_PID"
|
echo "[*] Server launched in background with PID $SERVER_PID"
|
||||||
|
|
||||||
# 3. Poll Hermes health / report endpoint until server is listening
|
# 3. Poll /health endpoint until server is listening
|
||||||
READY=0
|
READY=0
|
||||||
for i in $(seq 1 20); do
|
for i in $(seq 1 20); do
|
||||||
if curl -s http://127.0.0.1:8443/api/hermes/report >/dev/null 2>&1; then
|
if python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8443/health', timeout=1)" >/dev/null 2>&1; then
|
||||||
echo "[+] LOGAR Server is ready after ${i}s."
|
echo "[+] LOGAR Server is ready after ${i}s."
|
||||||
READY=1
|
READY=1
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,38 +1,72 @@
|
|||||||
name: Release Binaries & Installers
|
name: Release Binaries & Packages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Release tag to publish assets to (default: v2.0.1)'
|
description: 'Release tag to publish assets to (default: v0.3.1)'
|
||||||
required: false
|
required: false
|
||||||
default: 'v2.0.1'
|
default: 'v0.3.1'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-linux:
|
release:
|
||||||
name: Build & Release Linux Binaries
|
name: Build & Release Standalone Binaries (Linux & Windows Cross-Compilation)
|
||||||
runs-on: ubuntu-latest
|
runs-on: logar
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Python and Build Dependencies
|
- name: Install System and Cross-Compilation Build Dependencies
|
||||||
run: |
|
run: |
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
echo "=== SYSTEM DIAGNOSTICS ==="
|
||||||
apt-get update -y
|
uname -a || true
|
||||||
apt-get install -y python3 python3-pip python3-venv binutils zip
|
python3 --version || true
|
||||||
|
SUDO=""
|
||||||
|
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
||||||
|
SUDO="sudo"
|
||||||
fi
|
fi
|
||||||
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
pip3 install pyinstaller -r compilation/requirements.txt --break-system-packages || pip3 install pyinstaller -r compilation/requirements.txt
|
echo "[*] Installing Python shared libraries, headers, NSIS via apt-get..."
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
$SUDO apt-get clean || true
|
||||||
|
$SUDO apt-get update --allow-releaseinfo-change -o Acquire::Check-Valid-Until=false -y || $SUDO apt-get update -y || true
|
||||||
|
$SUDO apt-get -f install -y || true
|
||||||
|
PY_VER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "3.9")
|
||||||
|
echo "[*] Detected Python version: ${PY_VER}"
|
||||||
|
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends \
|
||||||
|
nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" \
|
||||||
|
python3-dev libpython3-dev build-essential gcc make curl tar gzip zip unzip || true
|
||||||
|
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends nsis nsis-common || true
|
||||||
|
for pkg in nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" python3-dev libpython3-dev build-essential; do
|
||||||
|
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends "$pkg" || true
|
||||||
|
done
|
||||||
|
echo "[*] Verifying makensis:"
|
||||||
|
which makensis || echo "makensis not in PATH"
|
||||||
|
echo "[*] Verifying libpython shared library:"
|
||||||
|
find /usr/lib /usr/local/lib -name "libpython*.so*" 2>/dev/null || true
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
echo "[*] Installing dependencies via apk..."
|
||||||
|
$SUDO apk add --no-cache nsis python3-dev build-base gcc make curl tar gzip zip unzip || true
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
echo "[*] Installing dependencies via dnf..."
|
||||||
|
$SUDO dnf install -y mingw32-nsis nsis python3-devel gcc make curl tar gzip zip unzip || true
|
||||||
|
fi
|
||||||
|
python3 compilation/ensure_pip.py
|
||||||
|
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
|
||||||
|
python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt
|
||||||
|
|
||||||
- name: Compile Standalone Linux Binaries
|
- name: Compile Standalone Binaries (Linux & Windows)
|
||||||
run: |
|
run: |
|
||||||
python3 compilation/package_dist.py --target linux
|
python3 compilation/package_dist.py --target all
|
||||||
|
|
||||||
- name: Publish Linux Release Assets
|
- name: Publish Unified Release Assets
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||||
@@ -40,85 +74,3 @@ jobs:
|
|||||||
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
|
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
python3 compilation/upload_release.py --skip-build
|
python3 compilation/upload_release.py --skip-build
|
||||||
|
|
||||||
release-windows:
|
|
||||||
name: Build & Release Windows Binaries & Installers
|
|
||||||
# Note: Requires a registered Gitea Act Runner with label 'windows-latest'
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$server = "${{ github.server_url }}"
|
|
||||||
$token = "${{ secrets.TAG_TOKEN || github.token }}"
|
|
||||||
$repo = "${{ github.repository }}"
|
|
||||||
$cleanUrl = $server -replace "^https?://", ""
|
|
||||||
$authUrl = "https://${token}@${cleanUrl}/${repo}.git"
|
|
||||||
|
|
||||||
if (-not (Test-Path ".git")) {
|
|
||||||
git init
|
|
||||||
git remote add origin $authUrl
|
|
||||||
} else {
|
|
||||||
git remote set-url origin $authUrl
|
|
||||||
}
|
|
||||||
git fetch --depth 1 origin "${{ github.sha }}"
|
|
||||||
git checkout -f FETCH_HEAD
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$py = "python"
|
|
||||||
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
|
|
||||||
if (Get-Command "py" -ErrorAction SilentlyContinue) {
|
|
||||||
$py = "py -3.12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& $py -m pip install --upgrade pip
|
|
||||||
& $py -m pip install pyinstaller cryptography -r compilation/requirements.txt
|
|
||||||
|
|
||||||
- name: Compile Standalone Windows Binaries
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$py = "python"
|
|
||||||
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
|
|
||||||
if (Get-Command "py" -ErrorAction SilentlyContinue) {
|
|
||||||
$py = "py -3.12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& $py compilation/package_dist.py --target windows
|
|
||||||
|
|
||||||
- name: Compile Inno Setup Installers
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$iscc = $null
|
|
||||||
if (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe") {
|
|
||||||
$iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
|
||||||
} elseif (Test-Path "C:\Program Files\Inno Setup 6\ISCC.exe") {
|
|
||||||
$iscc = "C:\Program Files\Inno Setup 6\ISCC.exe"
|
|
||||||
} elseif (Get-Command "ISCC.exe" -ErrorAction SilentlyContinue) {
|
|
||||||
$iscc = "ISCC.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($iscc) {
|
|
||||||
Write-Host "[*] Compiling Windows Inno Setup installers using $iscc..."
|
|
||||||
& $iscc compilation/installer_client.iss
|
|
||||||
& $iscc compilation/installer_server.iss
|
|
||||||
} else {
|
|
||||||
Write-Host "[!] Inno Setup compiler (ISCC.exe) not found on runner host. Skipping installer compilation."
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Publish Windows Release Assets
|
|
||||||
shell: powershell
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
|
||||||
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
|
|
||||||
run: |
|
|
||||||
$py = "python"
|
|
||||||
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
|
|
||||||
if (Get-Command "py" -ErrorAction SilentlyContinue) {
|
|
||||||
$py = "py -3.12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& $py compilation/upload_release.py --skip-build
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ server_config.json
|
|||||||
client_config.json
|
client_config.json
|
||||||
test_*.json
|
test_*.json
|
||||||
test_*.db
|
test_*.db
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
runner/.env
|
||||||
|
|
||||||
# IDE & OS files
|
# IDE & OS files
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
+1
-1
Submodule LOGAR-LICENSING updated: 6dc8ed20d5...7bdb7300ec
@@ -1,405 +1,170 @@
|
|||||||
# LOGAR: Edge-Thin Log Analysis & Temporal Verification System
|
# LOGAR: Fleet Log Aggregation & Centralized Provisioning System
|
||||||
|
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
|
||||||
|
|
||||||
**LOGAR** is an enterprise log aggregation, verification, and anomaly detection architecture designed for heterogeneous server fleets (Windows & Linux). It combines lightweight zero-state edge forwarders with a centralized cloud hub that applies mutual TLS 1.3 (**mTLS**) authentication, dynamic PKI licensing and quota management, temporal persistence tracking across 12-hour evaluation windows, and direct anomaly pass-through enabling **Hermes AI** to perform all deep reasoning, anomaly evaluation, and autonomous decision-making.
|
[](RELEASE_NOTES.md)
|
||||||
|
[](tests/)
|
||||||
|
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||||
|
|
||||||
|
**LOGAR** is an enterprise-grade log aggregation, normalization, and centralized provisioning system built for heterogeneous server fleets (Windows & Linux). It pairs ultra-lightweight, zero-state edge forwarders with a secure centralized server hub that enforces mutual TLS 1.3 (**mTLS**) authentication, dynamic PKI fleet licensing with cryptographic seat leasing, 12-hour temporal persistence tracking, and direct anomaly pass-through—delivering clean, structured, verified log telemetry directly to **Hermes AI** for autonomous deep reasoning, contextual root-cause analysis, and operational decision-making.
|
||||||
|
|
||||||
|
> 📚 **Comprehensive Technical Documentation**: Full architectural specifications, state machines, PKI X.509 OID definitions, and complete REST API references are maintained on the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Key Capabilities
|
||||||
1. [Core Philosophy](#core-philosophy)
|
|
||||||
2. [Architecture & Data Flow](#architecture--data-flow)
|
- **Zero-State Edge Forwarders**: Windows (`Win_Client`) and Linux (`Linux_Client`) agents maintain zero local databases, zero cache files, and minimal memory footprint.
|
||||||
3. [mTLS Security, Dynamic PKI & Licensing](#mtls-security-dynamic-pki--licensing)
|
- **Mutual TLS 1.3 (`:9443`)**: Enforces bidirectional cryptographic identity verification (`ssl.CERT_REQUIRED`) for all streaming log frames.
|
||||||
4. [Cloud-Side Temporal Persistence & Direct Hermes Reasoning](#cloud-side-temporal-persistence--direct-hermes-reasoning)
|
- **Hierarchical PKI & Seat Licensing**: Subordinate CAs automatically enroll and sync seat quotas with the Master Authority (`logar.eibl.tech`) over WebSockets.
|
||||||
5. [Agentic Hermes & Client Management API](#agentic-hermes--client-management-api)
|
- **12-Hour Temporal Persistence**: Evaluates anomalies over a rolling 12-hour temporal window, tracking occurrence velocity and first/last seen metrics.
|
||||||
6. [Dynamic Machine & Domain Identification](#dynamic-machine--domain-identification)
|
- **Direct Hermes AI Provisioning (`:8443`)**: Exposes verified anomalous telemetry directly to Hermes AI via `/api/hermes/report` without heuristic bottlenecks.
|
||||||
7. [Automated Service Installers (Linux & Windows)](#automated-service-installers-linux--windows)
|
- **WebUI Dashboard**: Authenticated, 2-tab management console for live anomaly telemetry, client seat leasing, and Sub-CA lifecycle management.
|
||||||
8. [Repository & Shippables Structure](#repository--shippables-structure)
|
|
||||||
9. [Getting Started & Deployment](#getting-started--deployment)
|
|
||||||
10. [Running Tests](#running-tests)
|
|
||||||
11. [Automated Releases via Gitea Actions](#automated-releases-via-gitea-actions)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Core Philosophy
|
## System Architecture
|
||||||
|
|
||||||
### 1. Edge Thinness & Zero State
|
|
||||||
Site agents running on Windows and Linux act strictly as lightweight forwarders:
|
|
||||||
- **No Local Database**: Clients maintain zero local SQLite or heavy cache files.
|
|
||||||
- **Source-Level Filtering**: Agents stream candidate entries from informational events up to errors (`INFO`, `WARNING`, `ERROR`, `CRITICAL`), stripping verbose debugging noise (`DEBUG`, trace entries) and skipping events older than 24 hours.
|
|
||||||
- **Transport Security (mTLS 1.3)**: Logs are streamed directly over mutual TLS 1.3 sockets with hardware-bound / machine-unique client certificates.
|
|
||||||
- **Zero Configuration Overhead**: Clients auto-bootstrap certificate enrollment on first run if configured with an enrollment secret.
|
|
||||||
|
|
||||||
### 2. Cloud-Side Temporal Persistence & Direct Hermes Reasoning
|
|
||||||
The central Python hub handles state and temporal tracking:
|
|
||||||
- State tracking is managed centrally in SQLite (`logar_state.db`).
|
|
||||||
- Candidate issues are tracked over a **12-hour temporal evaluation window** with consecutive run counts and first/last-seen timestamps.
|
|
||||||
- **Direct Hermes Intelligence**: All ingested actionable anomalies (`WARNING`, `ERROR`, `INFO`) are immediately promoted to `VERIFIED` and surfaced directly to Hermes. Hermes performs all multi-factor reasoning, pattern evaluation, and contextual thinking rather than relying on arbitrary hardcoded threshold rules.
|
|
||||||
|
|
||||||
### 3. Agentic Integration with Hermes
|
|
||||||
Instead of engineers manually sifting through logs, **Hermes** ingests verified anomalies directly from the cloud hub (`GET /api/hermes/report`), treating errors and warnings with full temporal metadata (run counts, timestamps) as actionable system artifacts to trigger precise notifications and remediations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture & Data Flow
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TB
|
graph TB
|
||||||
subgraph Edge Nodes [Lightweight Edge Forwarders]
|
subgraph Authority [Master License Authority - logar.eibl.tech]
|
||||||
W[Win_Client.exe / Win_Client.py<br/>Windows Event Log Ingestion]
|
M_CA[Master Root CA]
|
||||||
L[Linux_Client.bin / Linux_Client.py<br/>systemd journalctl -p warning]
|
AUTH_WS[WebSocket Licensing Hub]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Enrollment [Dynamic PKI & Licensing]
|
subgraph ServerHub [LOGAR Server Hub]
|
||||||
ENR[POST /api/client/enroll<br/>License Quota & Secret Validation]
|
SUB_CA[Subordinate CA]
|
||||||
CA[Internal Root CA<br/>Signs RSA-2048 Client Cert]
|
SEAT_MGR[Dynamic Seat Manager]
|
||||||
|
MTLS_SRV[mTLS 1.3 Ingestion Server<br/>Port 9443]
|
||||||
|
TEMP_ENG[12h Temporal Persistence Engine]
|
||||||
|
PROV_API[Hermes Provisioning & WebUI API<br/>Port 8443]
|
||||||
|
|
||||||
|
SUB_CA --> SEAT_MGR
|
||||||
|
SEAT_MGR --> MTLS_SRV
|
||||||
|
MTLS_SRV --> TEMP_ENG
|
||||||
|
TEMP_ENG --> PROV_API
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Transport [mTLS 1.3 Security Layer]
|
subgraph EdgeNodes [Heterogeneous Edge Forwarders]
|
||||||
MTLS[Mutual TLS 1.3 Handshake<br/>Port 9443 - Client Cert Required]
|
WIN[Win_Client.exe<br/>Windows Event Logs]
|
||||||
AUTH[Extract Client CN & Fingerprint<br/>Validate Active License in SQLite]
|
LIN[Linux_Client.bin<br/>systemd journalctl]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Hub [LOGAR Server Hub]
|
subgraph Downstream [Hermes AI Layer]
|
||||||
INGEST[Length-Prefixed Frame Ingestion]
|
HERMES[Hermes AI Engine]
|
||||||
DB[(SQLite Persistence<br/>active_issues, clients, license_config)]
|
|
||||||
RULE{12h Window &<br/>Temporal Ingestion}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Downstream [Hermes Agent & Monitoring]
|
M_CA -->|Signs Sub-CA Cert| SUB_CA
|
||||||
API[FastAPI Reporting & Management<br/>Port 8443]
|
AUTH_WS <==>|Live WebSocket Sync| SUB_CA
|
||||||
HERMES[Hermes Agent<br/>GET /api/hermes/report]
|
|
||||||
end
|
|
||||||
|
|
||||||
W -->|Auto-Enrollment| ENR
|
WIN -->|1. Auto-Enrollment| SUB_CA
|
||||||
L -->|Auto-Enrollment| ENR
|
LIN -->|1. Auto-Enrollment| SUB_CA
|
||||||
ENR --> CA
|
WIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
|
||||||
CA -->|ca.crt, client.crt, client.key| W
|
LIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
|
||||||
CA -->|ca.crt, client.crt, client.key| L
|
PROV_API ==>|3. GET /api/hermes/report| HERMES
|
||||||
|
|
||||||
W -->|mTLS Stream| MTLS
|
|
||||||
L -->|mTLS Stream| MTLS
|
|
||||||
MTLS --> AUTH
|
|
||||||
AUTH --> INGEST
|
|
||||||
INGEST --> RULE
|
|
||||||
RULE --> DB
|
|
||||||
DB --> API
|
|
||||||
API --> HERMES
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## mTLS Security, Dynamic PKI & Fleet Licensing
|
## Quick Start & Automated Installers
|
||||||
|
|
||||||
### 1. TLS 1.3 Mutual Authentication (mTLS)
|
LOGAR provides automated production service installers for both Linux (`systemd`) and Windows (`services.msc` via NSSM):
|
||||||
- **Port 9443**: Ingestion occurs exclusively over TLS 1.3 sockets with `ssl.CERT_REQUIRED`.
|
|
||||||
- Both the hub and edge clients verify each other's certificates:
|
|
||||||
- Client verifies server certificate against `ca.crt`.
|
|
||||||
- Server verifies client certificate against the Subordinate/Root CA.
|
|
||||||
- **Client CN Identification & Revocation**: In the TLS handshake, the server extracts the `commonName` attribute (`client_id`), validates that the client is marked `active` in the `clients` table, updates the `last_seen` timestamp, and drops unregistered or revoked certificates immediately.
|
|
||||||
|
|
||||||
### 2. Master License Authority (`logar.eibl.tech`), Multi-Tenancy & Subordinate CA
|
### 1. Server Hub Installation
|
||||||
- **Master Root Authority**: Standalone PKI repository & submodule ([`LOGAR-LICENSING/`](file:///u:/max/Desktop/LOGAR/LOGAR-LICENSING/README.md), upstream at [`https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git)) deployed at `logar.eibl.tech`.
|
|
||||||
- **Multi-Tenant Fleet Isolation & User Management**:
|
|
||||||
- Supports multiple isolated **Tenants** (e.g. `DEFAULT`, `CORP_A`, `PRODUCTION_FLEET`) with live tenant filtering.
|
|
||||||
- Built-in user management with PBKDF2 password hashing, RBAC (Administrators & Operators), and self-service password updates.
|
|
||||||
- **Custom Enterprise X.509 OIDs**:
|
|
||||||
- `OID_LOGAR_SEATS (1.3.6.1.4.1.59999.1.1)`: Embeds the exact licensed seat quota directly inside the signed Subordinate CA certificate.
|
|
||||||
- `OID_LOGAR_LICENSE_ID (1.3.6.1.4.1.59999.1.2)`: Uniquely tracks enterprise license entitlement.
|
|
||||||
- `OID_LOGAR_ISSUER_URL (1.3.6.1.4.1.59999.1.3)`: Embedded endpoint coordinate (`https://logar.eibl.tech`).
|
|
||||||
- **Bootstrap `.lic` Packages & Instant WebSocket Auto-Enrollment**:
|
|
||||||
- The License Authority (`logar.eibl.tech`) generates initial **Bootstrap `.lic` Packages** containing Master Root CA, dedicated communication credentials (`communication_cert` + `communication_key`), CSR templates, assigned tenant, and WebSocket coordinates bound directly to the authority server host.
|
|
||||||
- **1-Click Import & Live Signing**: Importing the `.lic` file on the client server (`python src/Server.py --import-lic <file.lic>` or via WebUI) connects directly via **WebSocket**, submits the server Sub-CA CSR, and immediately installs the officially signed Sub-CA license certificate.
|
|
||||||
- **Self-Updating Client Servers & Live WebSocket Sync**:
|
|
||||||
- Client servers update themselves against the master authority (`wss://logar.eibl.tech/ws/license`) over WebSocket (`python src/Server.py --sync-license` or via WebUI / automated watchdog).
|
|
||||||
- Seat quotas can be increased with each renewal or updated on-demand in the License Authority WebUI.
|
|
||||||
- The client server automatically receives the updated seat quota in its signed certificate, updates its SQLite database and runtime configuration, and reloads its in-flight `ssl.SSLContext` without dropping client connections or restarting services.
|
|
||||||
- **Automated Container Pipeline & Ephemeral NPM Pass-Through**:
|
|
||||||
- The license authority repository includes a dedicated CI/CD workflow ([`.gitea/workflows/build-container.yml`](file:///u:/max/Desktop/LOGAR/LOGAR-LICENSING/.gitea/workflows/build-container.yml)) publishing to `gitea.eibl.tech/me0nline/logar-licensing:latest` (authenticated via `giteatoken` secret).
|
|
||||||
- Works seamlessly with ephemeral Nginx Proxy Manager (NPM) by enabling the standard **Websockets Support** toggle for `logar.eibl.tech`.
|
|
||||||
- Admin WebUI is protected by authenticated login with user and tenant administration.
|
|
||||||
|
|
||||||
### 3. Integrated Fleet Licensing & WebUI Dashboard
|
- **Linux (Automated systemd Installer)**:
|
||||||
- **WebUI URL**: Accessible at `http://<server_host>:8443/webui` (or `/`).
|
|
||||||
- **Visual Seat Quota Meter**: Real-time progress bar displaying active forwarders vs. allocated seat limit.
|
|
||||||
- **Licensing Actions**:
|
|
||||||
- 1-click **Export .lic Request** file download.
|
|
||||||
- Interactive **View & Copy CA CSR** modal.
|
|
||||||
- Drag-and-drop or paste **Import License (.lic / ca.crt)** modal.
|
|
||||||
- **Sync & Renew via mTLS** button for immediate upstream synchronization and seat expansion.
|
|
||||||
- **Seat Management & Revocation Table**:
|
|
||||||
- Lists all connected Windows and Linux edge forwarders.
|
|
||||||
- **Revoke Seat**: Instantly drops and blocks the client at the TLS socket handshake layer.
|
|
||||||
- **Reactivate Seat**: Restores the client to active status if within seat quota limits.
|
|
||||||
|
|
||||||
### 4. Cloud-Side In-Flight Certificate Watchdog
|
|
||||||
- **Continuous Hub PKI Watchdog**:
|
|
||||||
- The server hub runs a continuous background watchdog coroutine (`cert_validity_watchdog`, running every 12 hours).
|
|
||||||
- Automatically renews Subordinate CA certificates over mTLS with `logar.eibl.tech` and Server TLS certificates in-flight, dynamically reloading the active `ssl.SSLContext`.
|
|
||||||
- **Client Proactive Check & Reactive Self-Healing**:
|
|
||||||
- **Proactive Renewal**: Edge clients inspect `client.crt` before every run cycle. If expiring within 30 days, they contact `/api/client/enroll` to renew.
|
|
||||||
- **Reactive Self-Healing**: If the server CA rotates or a handshake error occurs, edge clients catch the exception, re-bootstrap enrollment against the hub, and re-establish streaming cleanly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cloud-Side Temporal Persistence & Direct Hermes Reasoning
|
|
||||||
|
|
||||||
Incoming candidate logs are tracked in SQLite table `active_issues`:
|
|
||||||
- **Issue Fingerprint**: Formatted as `{site_name}:{server}:{signature}`.
|
|
||||||
- **12-Hour Evaluation Window**:
|
|
||||||
- The hub compares `(now - last_seen)`.
|
|
||||||
- If more than 12 hours have elapsed since the issue was last recorded, the previous window expires and the cycle resets to `run_count = 1`.
|
|
||||||
- **Direct Verification & Temporal Telemetry**:
|
|
||||||
- Ingested anomalies are immediately promoted to `VERIFIED` and made available to Hermes with complete temporal metadata (first seen, last seen, consecutive run counts).
|
|
||||||
- Hermes performs all contextual analysis, pattern recognition, and alerting logic.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agentic Hermes & Client Management API
|
|
||||||
|
|
||||||
The server hub exposes a management and reporting REST API (default port `8443`):
|
|
||||||
|
|
||||||
### `POST /api/client/enroll`
|
|
||||||
Client enrollment endpoint:
|
|
||||||
- **Request**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"client_id": "web-worker-01.corp.internal",
|
|
||||||
"hostname": "web-worker-01",
|
|
||||||
"os": "linux",
|
|
||||||
"enrollment_secret": "<SECRET>"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ca_cert": "-----BEGIN CERTIFICATE-----\n...",
|
|
||||||
"client_cert": "-----BEGIN CERTIFICATE-----\n...",
|
|
||||||
"client_key": "-----BEGIN RSA PRIVATE KEY-----\n..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `GET /api/clients`
|
|
||||||
Returns seat quota status and registered client telemetry:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"active_seats": 2,
|
|
||||||
"max_seats": 10,
|
|
||||||
"clients": [
|
|
||||||
{
|
|
||||||
"client_id": "web-worker-01.corp.internal",
|
|
||||||
"hostname": "web-worker-01",
|
|
||||||
"os_type": "linux",
|
|
||||||
"cert_fingerprint": "7D5B660B...",
|
|
||||||
"status": "active",
|
|
||||||
"first_seen": "2026-09-04 18:00:00",
|
|
||||||
"last_seen": "2026-09-04 19:15:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `GET /api/hermes/report`
|
|
||||||
Returns all **verified anomalies** within the active 12-hour evaluation window with full temporal metadata for direct Hermes reasoning:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"fingerprint": "corp.internal:web-worker-01.corp.internal:PostgresPoolExhausted",
|
|
||||||
"site": "corp.internal",
|
|
||||||
"server": "web-worker-01.corp.internal",
|
|
||||||
"signature": "PostgresPoolExhausted",
|
|
||||||
"severity": "WARNING",
|
|
||||||
"message": "Connection pool saturated (>95%)",
|
|
||||||
"os_type": "linux",
|
|
||||||
"first_seen": "2026-09-04T07:00:00+00:00",
|
|
||||||
"last_seen": "2026-09-04T19:00:00+00:00",
|
|
||||||
"consecutive_runs": 4,
|
|
||||||
"evaluation_window": "12h",
|
|
||||||
"verified": true,
|
|
||||||
"status": "VERIFIED"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `GET /api/hermes/all`
|
|
||||||
Diagnostic endpoint listing all tracked anomalies in SQLite.
|
|
||||||
|
|
||||||
### `GET /health`
|
|
||||||
Returns hub health, encryption fingerprint, listener ports, and mTLS status.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dynamic Machine & Domain Identification
|
|
||||||
|
|
||||||
Client configurations intentionally contain **no hardcoded machine name or site name**. Both forwarders dynamically identify their host and domain at runtime via `get_machine_identifier()`:
|
|
||||||
1. **Fully Qualified Domain Name (FQDN)**: Checked via `socket.getfqdn()`.
|
|
||||||
2. **OS-Specific Domain Discovery**:
|
|
||||||
- **Windows**: Checks Active Directory environment variables (`USERDNSDOMAIN`, `USERDOMAIN`).
|
|
||||||
- **Linux**: Parses `/etc/resolv.conf` `domain` and `search` directives.
|
|
||||||
3. **Reverse DNS Lookup**: Resolves canonical hostname via `socket.gethostbyaddr`.
|
|
||||||
4. **Fallback**: Local hostname `socket.gethostname()`.
|
|
||||||
|
|
||||||
The server automatically infers site attribution from domain qualifiers (e.g. `node01.corp.internal` $\rightarrow$ site `corp.internal`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Automated Service Installers (Linux & Windows)
|
|
||||||
|
|
||||||
LOGAR provides production-grade installation scripts and installer builders for automated service deployment:
|
|
||||||
|
|
||||||
### 1. Linux Service Installers
|
|
||||||
- **Client Installer (`compilation/install_linux_client.sh`)**:
|
|
||||||
- Non-interactive script deploying to `/opt/logar-client`.
|
|
||||||
- Automatically queries `/etc/machine-id` and enrolls with the hub via `curl`.
|
|
||||||
- Installs and enables `logar-client.service` systemd unit.
|
|
||||||
```bash
|
|
||||||
sudo ./compilation/install_linux_client.sh "http://hub.example.com:8443" "<ENROLLMENT_SECRET>"
|
|
||||||
```
|
|
||||||
- **Server Installer (`compilation/install_linux_server.sh`)**:
|
|
||||||
- Deploys server to `/opt/logar-server`.
|
|
||||||
- Configures logging and installs `logar-server.service` with `LimitNOFILE=65536`.
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./compilation/install_linux_server.sh
|
sudo ./compilation/install_linux_server.sh
|
||||||
```
|
```
|
||||||
|
*Deploys standalone `Server.bin` to `/opt/logar-server`, initializes PKI, and enables `logar-server.service`.*
|
||||||
|
|
||||||
### 2. Windows Inno Setup Installers
|
- **Windows (Inno Setup / NSIS Installer)**:
|
||||||
- Built using **Inno Setup 6** and bundled with **NSSM** (`compilation/nssm.exe`):
|
Download and execute `LOGAR-Server-Setup.exe` from [Releases](https://gitea.eibl.tech/me0nline/LOGAR/releases). Installs `Server.exe` into `C:\Program Files\LOGAR-Server` and registers `LOGAR_Server` Windows service.
|
||||||
- **Client Setup (`compilation/installer_client.iss`)**: Compiles `LOGAR-Client-Setup.exe`. Installs `Win_Client.exe` into `{autopf}\LOGAR`, sets up `LOGAR_Client` service via NSSM with stdout/stderr redirection to `{commonappdata}\LOGAR\client.log`, and starts the service. Clean uninstallation stops and removes the service.
|
|
||||||
- **Server Setup (`compilation/installer_server.iss`)**: Compiles `LOGAR-Server-Setup.exe`. Installs `Server.exe` and sets up `LOGAR_Server` Windows service via NSSM.
|
|
||||||
|
|
||||||
---
|
#### Default WebUI Management Credentials
|
||||||
|
Once installed or executed, access the web console:
|
||||||
|
- **URL**: `http://<server-ip>:8443/webui`
|
||||||
|
- **Default Username**: `admin`
|
||||||
|
- **Default Password**: `admin_logar_2026!` *(Configurable via `LOGAR_ADMIN_USER` / `LOGAR_ADMIN_PASS` or in the WebUI user settings)*
|
||||||
|
|
||||||
## Repository & Shippables Structure
|
### 2. Edge Forwarder Installation
|
||||||
|
|
||||||
```
|
- **Linux Client (1-Line Non-Interactive Installer)**:
|
||||||
LOGAR/
|
|
||||||
├── .gitea/
|
|
||||||
│ └── workflows/
|
|
||||||
│ ├── ci.yml # CI pipeline: syntax, 25 unit tests & mTLS pipeline test
|
|
||||||
│ └── release.yml # Consolidated release workflow (Linux binaries & Windows installers)
|
|
||||||
├── compilation/ # Packaging, installers, and release automation
|
|
||||||
│ ├── install_linux_client.sh # Automated Linux client systemd installation script
|
|
||||||
│ ├── install_linux_server.sh # Automated Linux server systemd installation script
|
|
||||||
│ ├── installer_client.iss # Inno Setup Windows Client installer script
|
|
||||||
│ ├── installer_server.iss # Inno Setup Windows Server installer script
|
|
||||||
│ ├── nssm.exe # Official 64-bit NSSM service manager binary
|
|
||||||
│ ├── package_dist.py # Standalone binary compiler & packager
|
|
||||||
│ ├── requirements.txt # Unified project dependencies
|
|
||||||
│ └── upload_release.py # Gitea REST API release asset publisher
|
|
||||||
├── src/ # Core application source modules
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── Server.py # Central mTLS server, temporal engine, and Hermes REST API
|
|
||||||
│ ├── server_enrollment.py # Dynamic PKI, Root CA, and client certificate generator
|
|
||||||
│ ├── Win_Client.py # Windows edge forwarder with auto-enrollment
|
|
||||||
│ └── Linux_Client.py # Linux edge forwarder with auto-enrollment
|
|
||||||
├── tests/ # Automated test suites
|
|
||||||
│ ├── test_linux_client.py # Linux client unit tests & mTLS certificate validation
|
|
||||||
│ ├── test_pipeline.py # End-to-end mTLS integration & direct anomaly verification test
|
|
||||||
│ ├── test_server.py # Server unit tests, PKI generation, and seat quota tests
|
|
||||||
│ └── test_win_client.py # Windows client unit tests & mTLS certificate validation
|
|
||||||
├── .gitignore # Ignores venv, caches, DBs, and private keys
|
|
||||||
├── README.md # Architecture and usage documentation
|
|
||||||
├── RELEASE_NOTES.md # Release history and changelog
|
|
||||||
├── server_config.sample.json # Reference server configuration
|
|
||||||
└── out/ # Component guides and sample configs
|
|
||||||
├── linux_server/
|
|
||||||
│ ├── README.md
|
|
||||||
│ └── server_config.sample.json
|
|
||||||
├── win_server/
|
|
||||||
│ ├── README.md
|
|
||||||
│ └── server_config.sample.json
|
|
||||||
├── linux_client/
|
|
||||||
│ ├── README.md
|
|
||||||
│ └── client_config.sample.json
|
|
||||||
└── win_client/
|
|
||||||
├── README.md
|
|
||||||
└── client_config.sample.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Started & Deployment
|
|
||||||
|
|
||||||
### 1. Central Server Hub
|
|
||||||
|
|
||||||
1. **Install dependencies**:
|
|
||||||
```bash
|
|
||||||
pip install -r compilation/requirements.txt
|
|
||||||
```
|
|
||||||
2. **Start the server** (generates `server_config.json`, Root CA, and server certs on first run):
|
|
||||||
```bash
|
|
||||||
python src/Server.py
|
|
||||||
```
|
|
||||||
3. **Export a client configuration**:
|
|
||||||
```bash
|
|
||||||
python src/Server.py --create-client-config --server-host <SERVER_IP> --server-port 9443 --client-out client_config.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Windows Client Deployment
|
|
||||||
1. Download `LOGAR-Client-Setup.exe` from releases and run it, or place `Win_Client.exe` and `client_config.json` in `C:\Program Files\LOGAR`.
|
|
||||||
2. On first run with `client_config.json`, `Win_Client.exe` automatically enrolls with the hub, receives its mTLS certificates, and establishes secure streaming.
|
|
||||||
|
|
||||||
### 3. Linux Client Deployment
|
|
||||||
1. Run the automated installer:
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./compilation/install_linux_client.sh "http://<HUB_HOST>:8443" "<ENROLLMENT_SECRET>"
|
sudo ./compilation/install_linux_client.sh "http://<HUB_HOST>:8443" "<ENROLLMENT_SECRET>"
|
||||||
```
|
```
|
||||||
2. The installer enrolls the client, configures `/etc/logar/certs`, and activates `logar-client.service`.
|
*Auto-enrolls mTLS certificates to `/etc/logar/certs` (`chmod 700`), generates configuration, and activates `logar-client.service`.*
|
||||||
|
|
||||||
|
- **Windows Client (Inno Setup Installer)**:
|
||||||
|
1. Download and run `LOGAR-Client-Setup.exe` (supports silent install `/VERYSILENT`).
|
||||||
|
2. Place `client_config.json` (exported from Server WebUI) into `C:\Program Files\LOGAR\client_config.json`.
|
||||||
|
3. The `LOGAR_Client` service starts and auto-enrolls immediately.
|
||||||
|
|
||||||
|
*(Alternatively, standalone binaries `./Server.bin` / `./Linux_Client.bin` / `.\Server.exe` / `.\Win_Client.exe` can be executed directly without service installation).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Running Tests
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
LOGAR/
|
||||||
|
├── .gitea/workflows/ # CI/CD pipelines (ci.yml, release.yml)
|
||||||
|
├── compilation/ # Build scripts, Linux installers & Inno Setup scripts
|
||||||
|
│ ├── install_linux_client.sh # Linux client systemd auto-installer
|
||||||
|
│ ├── install_linux_server.sh # Linux server systemd auto-installer
|
||||||
|
│ ├── package_dist.py # Standalone binary cross-compiler
|
||||||
|
│ └── upload_release.py # Gitea release asset publisher
|
||||||
|
├── src/ # Application source code
|
||||||
|
│ ├── Server.py # Central mTLS hub, temporal engine & REST API
|
||||||
|
│ ├── server_enrollment.py # Sub-CA PKI and certificate generator
|
||||||
|
│ ├── Win_Client.py # Windows edge forwarder
|
||||||
|
│ └── Linux_Client.py # Linux edge forwarder
|
||||||
|
├── tests/ # Automated test suites (35 unit tests + pipeline test)
|
||||||
|
├── LOGAR-LICENSING/ # Standalone Master License Authority (Git submodule)
|
||||||
|
├── RELEASE_NOTES.md # Version history and security changelog
|
||||||
|
└── README.md # Repository overview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ports & REST API Summary
|
||||||
|
|
||||||
|
| Port | Protocol | Service | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **`9443`** | `TCP / TLS 1.3` | **mTLS Log Ingestion** | Mutual TLS socket listener for edge forwarders |
|
||||||
|
| **`8443`** | `HTTP / REST` | **Management & Hermes API** | WebUI, forwarder enrollment, seat management, and AI reports |
|
||||||
|
|
||||||
|
### Core Endpoints
|
||||||
|
|
||||||
|
- **`POST /api/client/enroll`**: Forwarder auto-enrollment and proactive certificate renewal.
|
||||||
|
- **`GET /api/clients`**: Returns active seat quota and registered edge forwarder telemetry *(Admin Auth)*.
|
||||||
|
- **`GET /api/hermes/report`**: Structured JSON anomaly stream for Hermes AI reasoning.
|
||||||
|
- **`POST /api/license/sync`**: Triggers immediate WebSocket sync with `logar.eibl.tech` *(Admin Auth)*.
|
||||||
|
- **`GET /health`**: Server health, encryption fingerprint, and mTLS status.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing & Cross-Compilation
|
||||||
|
|
||||||
### 1. Component Unit Tests
|
|
||||||
```bash
|
```bash
|
||||||
|
# Run full unit test suite (35 tests)
|
||||||
python -m unittest discover -s tests -v
|
python -m unittest discover -s tests -v
|
||||||
```
|
|
||||||
Runs all **25 unit tests**, covering:
|
|
||||||
- Dynamic Root CA generation and server TLS certificate issuance.
|
|
||||||
- Dynamic client certificate issuance with CN and authority key extensions.
|
|
||||||
- Enrollment secret authentication, seat limits, and certificate revocation.
|
|
||||||
- Proactive certificate validity checks, Root CA auto-renewal, and in-flight server SSLContext reload.
|
|
||||||
- Windows & Linux event log collection, deduplication, and mTLS certificate verification.
|
|
||||||
- 12-hour evaluation window and direct Hermes anomaly verification.
|
|
||||||
|
|
||||||
### 2. End-to-End Pipeline Integration Test
|
# Run end-to-end mTLS pipeline integration test
|
||||||
```bash
|
|
||||||
# 1. Initialize test configuration
|
|
||||||
python src/Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
|
|
||||||
|
|
||||||
# 2. Launch server in background
|
|
||||||
python src/Server.py &
|
|
||||||
|
|
||||||
# 3. Run integration test
|
|
||||||
python tests/test_pipeline.py
|
python tests/test_pipeline.py
|
||||||
|
|
||||||
|
# Cross-compile native Linux and Windows binaries on Linux runner
|
||||||
|
python compilation/package_dist.py --target all
|
||||||
```
|
```
|
||||||
Tests client enrollment, secret rejection, mTLS TLS 1.3 socket handshake, direct warning promotion, immediate error promotion, and Hermes report output.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Releases & Binary Distribution
|
## Documentation & Deep Dives
|
||||||
|
|
||||||
Releases can be built and published through three complementary channels:
|
For in-depth operational guides, lifecycle state machines, and PKI specifications, consult the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**:
|
||||||
|
|
||||||
### 1. Tag Push Automation (Gitea Actions)
|
* [Architecture & Design Details](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Architecture-and-Design)
|
||||||
Pushing a release tag automatically triggers the build workflows:
|
* [System State Machines & Subsystem Lifecycles](https://gitea.eibl.tech/me0nline/LOGAR/wiki/State-Machines-and-Lifecycles)
|
||||||
```bash
|
* [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||||
git tag v2.0.1
|
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
|
||||||
git push origin v2.0.1
|
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
|
||||||
```
|
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
|
||||||
The consolidated workflow **`release.yml`** defines two parallel jobs:
|
|
||||||
- **`release-linux`** (`ubuntu-latest`): Compiles standalone native ELF binaries (`Linux_Client.bin`, `Server.bin`) and checksums.
|
|
||||||
- **`release-windows`** (`windows-latest`): Compiles Windows executables (`Win_Client.exe`, `Server.exe`), builds Inno Setup installers, and publishes checksums (requires self-hosted Windows Act Runner).
|
|
||||||
|
|
||||||
### 2. Manual Workflow Dispatch (Gitea UI)
|
|
||||||
Workflows can be manually triggered on demand from the Gitea web interface:
|
|
||||||
1. Navigate to **Actions** $\rightarrow$ **Release Binaries & Installers** (`release.yml`).
|
|
||||||
2. Click **Run workflow**, set the release tag (defaults to `v2.0.1`), and run.
|
|
||||||
|
|
||||||
### 3. Native Local Windows Build & Direct Release Upload
|
|
||||||
For environments without a registered Windows CI runner, Windows executables can be built and published directly to Gitea releases:
|
|
||||||
```powershell
|
|
||||||
# 1. Build Windows binaries locally
|
|
||||||
python compilation/package_dist.py --target windows
|
|
||||||
|
|
||||||
# 2. Upload assets and release notes directly to the Gitea release
|
|
||||||
python compilation/upload_release.py --tag v2.0.1 --token <GITEA_TOKEN> --skip-build
|
|
||||||
```
|
|
||||||
|
|||||||
+11
-58
@@ -1,60 +1,13 @@
|
|||||||
# LOGAR Release v2.0.1
|
# LOGAR Release v0.3.1
|
||||||
|
|
||||||
Maintenance and deployment release consolidating Gitea Actions release automation into a unified single workflow file, standardizing release dispatching across platforms, and bumping version definitions across server hub and Windows installers.
|
### 🛡️ Security Fixes & Defensive Hardening
|
||||||
|
- **Mandatory Admin Authentication**: Enforced strict administrator authentication across all fleet management endpoints (`/api/clients`, `/api/clients/approve`, `/api/clients/revoke`, `/api/clients/delete`, `/api/client/export-profile`) and license management APIs.
|
||||||
### Key Highlights & Changes in v2.0.1:
|
- **Brute-Force Mitigation & Rate Limiting**: Implemented thread-safe sliding-window IP rate limiting on `/login` (10 attempts/min) and `/api/client/enroll` (15 attempts/min) with progressive delays and `HTTP 429` throttling.
|
||||||
|
- **Cookie Security Flags**: Session cookies now enforce `HttpOnly=True`, `SameSite="Lax"`, and dynamic `Secure=True` for HTTPS connections and reverse-proxy deployments (`X-Forwarded-Proto`).
|
||||||
- **Consolidated Single Release Automation Workflow (`.gitea/workflows/release.yml`)**:
|
- **Cryptographic Random Secrets**: Server Hub and License Authority session secrets are now generated with 256-bit CSPRNG (`secrets.token_hex(32)`) on initialization.
|
||||||
- Unified separate platform release files into a single, cohesive workflow (`release.yml`) running parallel jobs (`release-linux` on `ubuntu-latest` and `release-windows` on `windows-latest`).
|
- **License Renewal Validation**: Hardened `/api/license/renew` with explicit PEM structure and database validation, preventing unhandled exceptions.
|
||||||
- Standardized tag matching for Gitea Actions on `push: tags: ['v*']`.
|
|
||||||
- Added streamlined manual `workflow_dispatch` triggers with automated release tag defaulting (`v2.0.1`).
|
|
||||||
- Retired deprecated `release-linux.yml` and `release-windows.yml` files.
|
|
||||||
|
|
||||||
- **Installer & Engine Version Bump**:
|
|
||||||
- Updated FastAPI Hub engine version to `2.0.1` in `src/Server.py`.
|
|
||||||
- Bumped Inno Setup Windows Client Installer (`compilation/installer_client.iss`) `AppVersion` to `2.0.1`.
|
|
||||||
- Bumped Inno Setup Windows Server Installer (`compilation/installer_server.iss`) `AppVersion` to `2.0.1`.
|
|
||||||
|
|
||||||
- **Distribution & Release Documentation**:
|
|
||||||
- Updated root and distribution documentation across all 5 deployment targets to reflect the 25 passing unit tests and tripartite release options.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# LOGAR Release v2.0.0
|
|
||||||
|
|
||||||
Major architectural release introducing Mutual TLS 1.3 (mTLS) transport security, built-in dynamic PKI & license accounting, in-flight certificate validity monitoring and auto-renewal, and automated Windows and Linux service installers.
|
|
||||||
|
|
||||||
### Key Highlights & Changes in v2.0.0:
|
|
||||||
|
|
||||||
- **mTLS 1.3 Transport Security & Runtime Licensing**:
|
|
||||||
- Replaced legacy plain TCP sockets with mutual TLS 1.3 authentication (`ssl.CERT_REQUIRED`, TLS 1.3 minimum version).
|
|
||||||
- Hub dynamically validates incoming client Common Name (`client_id`) against active license seats in SQLite during the TLS handshake.
|
|
||||||
- Drops unauthorized, un-enrolled, or revoked clients at the transport layer before payload reading.
|
|
||||||
|
|
||||||
- **Dynamic Hub PKI Engine (`src/server_enrollment.py`)**:
|
|
||||||
- Automatically initializes an internal RSA-4096 Root CA (`ca.crt` / `ca.key`).
|
|
||||||
- Generates RSA-2048 Server TLS certificates with SANs for localhost, loopback, and server hostnames.
|
|
||||||
- Full OpenSSL 3.x and Python 3.12–3.14 compatibility via `SubjectKeyIdentifier` and `AuthorityKeyIdentifier` certificate extensions.
|
|
||||||
- Generates and signs client certificates on demand via `POST /api/client/enroll`.
|
|
||||||
|
|
||||||
- **In-Flight Certificate Validity Watchdog & Dynamic SSLContext Reloading**:
|
|
||||||
- Server hub runs a continuous background watchdog coroutine (`cert_validity_watchdog`, evaluated every 12 hours) alongside startup checks.
|
|
||||||
- Automatically checks Root CA and server TLS certificate expiration against a 30-day threshold.
|
|
||||||
- Generates renewed certificates on disk with timestamped backups (`.bak`), and reloads active `ssl.SSLContext` in memory dynamically without dropping socket listeners or restarting the background service.
|
|
||||||
|
|
||||||
- **Client Proactive Expiry Check & Reactive Self-Healing Auto-Renewal**:
|
|
||||||
- **Proactive**: Forwarders (`Win_Client.py` and `Linux_Client.py`) evaluate `client.crt` validity before each run, auto-renewing via `/api/client/enroll` if expiring within 30 days.
|
|
||||||
- **Reactive**: If the hub rotates its Root CA or a TLS verification error (`ssl.SSLError` / `SSLCertVerificationError`) occurs, clients automatically catch the error, re-enroll with the hub using their enrollment secret, and reconnect cleanly.
|
|
||||||
|
|
||||||
- **Database Schema & License Quota Accounting**:
|
|
||||||
- SQLite tables `license_config` (`max_seats`, `enrollment_secret`) and `clients` (`client_id`, `hostname`, `os_type`, `cert_fingerprint`, `status`, timestamps).
|
|
||||||
- Enforces seat limits on enrollment (`HTTP 403 License seat limit reached`) while allowing active registered nodes to re-enroll/renew indefinitely.
|
|
||||||
- Added `GET /api/clients` endpoint for license auditing and telemetry tracking.
|
|
||||||
|
|
||||||
- **Automated Service Installers**:
|
|
||||||
- **Windows**: Self-contained Inno Setup installers (`LOGAR-Client-Setup.exe` and `LOGAR-Server-Setup.exe`) bundling `nssm.exe` to register, configure, and start Windows services automatically.
|
|
||||||
- **Linux**: Automated installer scripts (`compilation/install_linux_client.sh` and `install_linux_server.sh`) deploying systemd service units with auto-restart policies.
|
|
||||||
|
|
||||||
- **CI/CD Release Workflows**:
|
|
||||||
- Windows workflow (`.gitea/workflows/release-windows.yml`) and Linux workflow (`.gitea/workflows/release-linux.yml`) automated to build native executables, installers, and upload release assets on tag push.
|
|
||||||
|
|
||||||
|
### ⚡ Operational Improvements & Modernization
|
||||||
|
- **FastAPI Lifespan Handlers**: Migrated legacy startup events to modern ASGI lifespan context managers.
|
||||||
|
- **OpenPGP Compression Preferences**: Aligned Server public key compression algorithms (`ZIP`, `ZLIB`, `BZ2`, `Uncompressed`) with client preferences.
|
||||||
|
- **Cross-Platform Compilation**: Unified automated packaging for standalone Windows and Linux binaries.
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
import pip # noqa: F401
|
||||||
|
print("[+] pip is already installed.")
|
||||||
|
return
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ensurepip
|
||||||
|
ensurepip.bootstrap()
|
||||||
|
print("[+] Successfully bootstrapped pip via ensurepip.")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[*] ensurepip bootstrap skipped ({e}), downloading get-pip.py...")
|
||||||
|
|
||||||
|
ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||||
|
url = f"https://bootstrap.pypa.io/pip/{ver}/get-pip.py" if sys.version_info < (3, 10) else "https://bootstrap.pypa.io/get-pip.py"
|
||||||
|
dest = "/tmp/get-pip.py" if sys.platform != "win32" else "get-pip.py"
|
||||||
|
|
||||||
|
print(f"[*] Downloading get-pip.py from {url} for Python {ver}...")
|
||||||
|
try:
|
||||||
|
urllib.request.urlretrieve(url, dest)
|
||||||
|
cmd = [sys.executable, dest]
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
cmd.append("--break-system-packages")
|
||||||
|
res = subprocess.run(cmd)
|
||||||
|
if res.returncode == 0:
|
||||||
|
print("[+] Successfully installed pip via get-pip.py")
|
||||||
|
else:
|
||||||
|
# Retry without break-system-packages in case old pip version rejected it
|
||||||
|
subprocess.run([sys.executable, dest])
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"[!] Warning during pip bootstrap: {ex}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -9,6 +9,7 @@ CONFIG_DIR="/etc/logar"
|
|||||||
|
|
||||||
echo "[+] Installing LOGAR Client..."
|
echo "[+] Installing LOGAR Client..."
|
||||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
|
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
|
||||||
|
chmod 700 "${CONFIG_DIR}/certs"
|
||||||
|
|
||||||
if [ -f "dist/Linux_Client.bin" ]; then
|
if [ -f "dist/Linux_Client.bin" ]; then
|
||||||
cp dist/Linux_Client.bin "${INSTALL_DIR}/Linux_Client"
|
cp dist/Linux_Client.bin "${INSTALL_DIR}/Linux_Client"
|
||||||
|
|||||||
@@ -4,7 +4,31 @@ set -euo pipefail
|
|||||||
INSTALL_DIR="/opt/logar-server"
|
INSTALL_DIR="/opt/logar-server"
|
||||||
CONFIG_DIR="/etc/logar"
|
CONFIG_DIR="/etc/logar"
|
||||||
|
|
||||||
echo "[+] Installing LOGAR Server..."
|
# Parse command line options
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--config-dir|--data-dir)
|
||||||
|
CONFIG_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo " LOGAR Server Enterprise Linux Installer"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
# Interactive configuration if running in a terminal without CLI arguments
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
read -r -p "Enter Data and Configuration Directory [${CONFIG_DIR}]: " USER_CFG
|
||||||
|
if [ -n "${USER_CFG}" ]; then
|
||||||
|
CONFIG_DIR="${USER_CFG}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
|
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
|
||||||
|
|
||||||
if [ -f "dist/Server.bin" ]; then
|
if [ -f "dist/Server.bin" ]; then
|
||||||
|
|||||||
@@ -1,27 +1,97 @@
|
|||||||
[Setup]
|
[Setup]
|
||||||
AppName=LOGAR Client
|
AppName=LOGAR Client
|
||||||
AppVersion=2.0.1
|
AppVersion=0.3.1
|
||||||
|
AppPublisher=LOGAR Security Systems
|
||||||
|
AppPublisherURL=https://logar.eibl.tech
|
||||||
|
AppSupportURL=https://logar.eibl.tech
|
||||||
|
AppUpdatesURL=https://logar.eibl.tech
|
||||||
DefaultDirName={autopf}\LOGAR
|
DefaultDirName={autopf}\LOGAR
|
||||||
OutputDir=..\dist
|
OutputDir=..\dist
|
||||||
OutputBaseFilename=LOGAR-Client-Setup
|
OutputBaseFilename=LOGAR-Client-Setup
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=admin
|
||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
VersionInfoCompany=LOGAR Security Systems
|
||||||
|
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
|
||||||
|
VersionInfoVersion=0.3.1.0
|
||||||
|
VersionInfoProductName=LOGAR Client
|
||||||
|
VersionInfoProductVersion=0.3.1
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Dirs]
|
[Dirs]
|
||||||
Name: "{commonappdata}\LOGAR"; Permissions: users-modify
|
Name: "{code:GetDataDir}"; Permissions: users-modify
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Client ""{app}\Win_Client.exe"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Client ""{app}\Win_Client.exe"""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppDirectory ""{app}"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppDirectory ""{app}"""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStdout ""{commonappdata}\LOGAR\client.log"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStdout ""{code:GetDataDir}\client.log"""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStderr ""{commonappdata}\LOGAR\client_err.log"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStderr ""{code:GetDataDir}\client_err.log"""; Flags: runhidden
|
||||||
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppParameters ""--config """"{code:GetDataDir}\client_config.json"""""""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Client"; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Client"; Flags: runhidden
|
||||||
|
|
||||||
[UninstallRun]
|
[UninstallRun]
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Client"; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Client"; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Client confirm"; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Client confirm"; Flags: runhidden
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{app}\certs"
|
||||||
|
Type: files; Name: "{app}\client_config.json"
|
||||||
|
Type: files; Name: "{app}\client_state.json"
|
||||||
|
Type: files; Name: "{app}\*.log"
|
||||||
|
Type: filesandordirs; Name: "{app}\runtime"
|
||||||
|
Type: filesandordirs; Name: "{app}"
|
||||||
|
Type: filesandordirs; Name: "{commonappdata}\LOGAR"
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
var
|
||||||
|
DataDirPage: TInputDirWizardPage;
|
||||||
|
|
||||||
|
procedure InitializeWizard;
|
||||||
|
begin
|
||||||
|
DataDirPage := CreateInputDirPage(
|
||||||
|
wpSelectDir,
|
||||||
|
'Data and Configuration Location',
|
||||||
|
'Where should LOGAR Client store its state tracking and certificates?',
|
||||||
|
'Select the folder where LOGAR Client should store state tracking (client_state.json), configuration (client_config.json), and mTLS certificates.'#13#10#13#10 +
|
||||||
|
'By default, this is stored in Common AppData (C:\ProgramData\LOGAR), which is accessible to background Windows services.',
|
||||||
|
False,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
DataDirPage.Add('Data Directory:');
|
||||||
|
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
|
||||||
|
end;
|
||||||
|
|
||||||
|
function GetDataDir(Param: String): String;
|
||||||
|
begin
|
||||||
|
Result := DataDirPage.Values[0];
|
||||||
|
if Result = '' then
|
||||||
|
Result := ExpandConstant('{commonappdata}\LOGAR');
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
begin
|
||||||
|
if CurStep = ssPostInstall then
|
||||||
|
begin
|
||||||
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||||
|
var
|
||||||
|
CustomDataDir: String;
|
||||||
|
begin
|
||||||
|
if CurUninstallStep = usPostUninstall then
|
||||||
|
begin
|
||||||
|
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', CustomDataDir) then
|
||||||
|
begin
|
||||||
|
if (CustomDataDir <> '') and DirExists(CustomDataDir) then
|
||||||
|
DelTree(CustomDataDir, True, True, True);
|
||||||
|
RegDeleteKeyIncludingSubkeys(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
!include "MUI2.nsh"
|
||||||
|
!include "LogicLib.nsh"
|
||||||
|
|
||||||
|
Name "LOGAR Client"
|
||||||
|
OutFile "../dist/LOGAR-Client-Setup.exe"
|
||||||
|
InstallDir "$PROGRAMFILES64\LOGAR"
|
||||||
|
RequestExecutionLevel admin
|
||||||
|
|
||||||
|
BrandingText "LOGAR Enterprise SIEM & Edge Forwarder"
|
||||||
|
|
||||||
|
VIProductVersion "0.3.1.0"
|
||||||
|
VIAddVersionKey "ProductName" "LOGAR Client"
|
||||||
|
VIAddVersionKey "CompanyName" "LOGAR Security Systems"
|
||||||
|
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR Security Systems"
|
||||||
|
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
|
||||||
|
VIAddVersionKey "FileVersion" "0.3.1.0"
|
||||||
|
VIAddVersionKey "ProductVersion" "0.3.1.0"
|
||||||
|
|
||||||
|
Var DATADIR
|
||||||
|
|
||||||
|
# --- UI Pages ---
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
|
||||||
|
# Welcome Page
|
||||||
|
!define MUI_WELCOMEPAGE_TITLE "Welcome to LOGAR Client Setup"
|
||||||
|
!define MUI_WELCOMEPAGE_TEXT "LOGAR Client provides secure, encrypted Windows Event Log forwarding with automatic certificate enrollment and state tracking.\r\n\r\nThis wizard will guide you through installing the forwarder service."
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
|
||||||
|
# Directory Page 1: Installation Directory
|
||||||
|
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
||||||
|
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
||||||
|
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
# Directory Page 2: Data Directory
|
||||||
|
!define MUI_PAGE_HEADER_TEXT "Choose Data & Storage Location"
|
||||||
|
!define MUI_PAGE_HEADER_SUBTEXT "Choose where state tracking, certificates, and configuration files should be stored."
|
||||||
|
!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose the folder where LOGAR Client should store state tracking (client_state.json), configuration (client_config.json), and mTLS certificates.$\r$\n$\r$\nDefault is recommended for automatic background service access."
|
||||||
|
!define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data & Storage Folder"
|
||||||
|
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
# Instfiles Page
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
|
# Finish Page
|
||||||
|
!define MUI_FINISHPAGE_TITLE "Installation Complete"
|
||||||
|
!define MUI_FINISHPAGE_TEXT "LOGAR Client has been successfully installed and configured as an automated background service."
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
# Uninstaller Pages
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
SetShellVarContext all
|
||||||
|
StrCpy $DATADIR "$APPDATA\LOGAR"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section "Install"
|
||||||
|
SetShellVarContext all
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
File "../dist/Win_Client.exe"
|
||||||
|
File "../compilation/nssm.exe"
|
||||||
|
File /r "../dist/runtime"
|
||||||
|
|
||||||
|
CreateDirectory "$DATADIR"
|
||||||
|
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
||||||
|
|
||||||
|
# Install and configure service via NSSM
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Client "$INSTDIR\Win_Client.exe"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppDirectory "$INSTDIR"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$DATADIR\client.log"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$DATADIR\client_err.log"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppParameters "--config \"$DATADIR\client_config.json\""'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
|
||||||
|
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
WriteRegStr HKLM "Software\LOGAR\LOGAR-Client" "DataDir" "$DATADIR"
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Uninstall"
|
||||||
|
SetShellVarContext all
|
||||||
|
ReadRegStr $DATADIR HKLM "Software\LOGAR\LOGAR-Client" "DataDir"
|
||||||
|
${If} $DATADIR == ""
|
||||||
|
StrCpy $DATADIR "$APPDATA\LOGAR"
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm'
|
||||||
|
RMDir /r "$INSTDIR\runtime"
|
||||||
|
RMDir /r "$INSTDIR\certs"
|
||||||
|
Delete "$INSTDIR\client_config.json"
|
||||||
|
Delete "$INSTDIR\client_state.json"
|
||||||
|
Delete "$INSTDIR\*.log"
|
||||||
|
Delete "$INSTDIR\Win_Client.exe"
|
||||||
|
Delete "$INSTDIR\nssm.exe"
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
RMDir /r "$INSTDIR"
|
||||||
|
RMDir /r "$DATADIR"
|
||||||
|
DeleteRegKey HKLM "Software\LOGAR\LOGAR-Client"
|
||||||
|
SectionEnd
|
||||||
@@ -1,27 +1,98 @@
|
|||||||
[Setup]
|
[Setup]
|
||||||
AppName=LOGAR Server
|
AppName=LOGAR Server
|
||||||
AppVersion=2.0.1
|
AppVersion=0.3.1
|
||||||
|
AppPublisher=LOGAR Security Systems
|
||||||
|
AppPublisherURL=https://logar.eibl.tech
|
||||||
|
AppSupportURL=https://logar.eibl.tech
|
||||||
|
AppUpdatesURL=https://logar.eibl.tech
|
||||||
DefaultDirName={autopf}\LOGAR-Server
|
DefaultDirName={autopf}\LOGAR-Server
|
||||||
OutputDir=..\dist
|
OutputDir=..\dist
|
||||||
OutputBaseFilename=LOGAR-Server-Setup
|
OutputBaseFilename=LOGAR-Server-Setup
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=admin
|
||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
VersionInfoCompany=LOGAR Security Systems
|
||||||
|
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
|
||||||
|
VersionInfoVersion=0.3.1.0
|
||||||
|
VersionInfoProductName=LOGAR Server
|
||||||
|
VersionInfoProductVersion=0.3.1
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Dirs]
|
[Dirs]
|
||||||
Name: "{commonappdata}\LOGAR-Server"; Permissions: users-modify
|
Name: "{code:GetDataDir}"; Permissions: users-modify
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Server ""{app}\Server.exe"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Server ""{app}\Server.exe"""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppDirectory ""{app}"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppDirectory ""{app}"""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStdout ""{commonappdata}\LOGAR-Server\server.log"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStdout ""{code:GetDataDir}\server.log"""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStderr ""{commonappdata}\LOGAR-Server\server_err.log"""; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStderr ""{code:GetDataDir}\server_err.log"""; Flags: runhidden
|
||||||
|
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppParameters ""--config """"{code:GetDataDir}\server_config.json"""""""; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Server"; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Server"; Flags: runhidden
|
||||||
|
|
||||||
[UninstallRun]
|
[UninstallRun]
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Server"; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Server"; Flags: runhidden
|
||||||
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Server confirm"; Flags: runhidden
|
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Server confirm"; Flags: runhidden
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{app}\certs"
|
||||||
|
Type: files; Name: "{app}\server_config.json"
|
||||||
|
Type: files; Name: "{app}\logar_state.db"
|
||||||
|
Type: files; Name: "{app}\logar_state.db-*"
|
||||||
|
Type: files; Name: "{app}\*.log"
|
||||||
|
Type: filesandordirs; Name: "{app}\runtime"
|
||||||
|
Type: filesandordirs; Name: "{app}"
|
||||||
|
Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server"
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
var
|
||||||
|
DataDirPage: TInputDirWizardPage;
|
||||||
|
|
||||||
|
procedure InitializeWizard;
|
||||||
|
begin
|
||||||
|
DataDirPage := CreateInputDirPage(
|
||||||
|
wpSelectDir,
|
||||||
|
'Data and Configuration Location',
|
||||||
|
'Where should LOGAR Server store its database, certificates, and configuration?',
|
||||||
|
'Select the folder where LOGAR Server should store the database (logar_state.db), configuration (server_config.json), and mTLS certificates.'#13#10#13#10 +
|
||||||
|
'By default, this is stored in Common AppData (C:\ProgramData\LOGAR-Server), which is accessible to background Windows services.',
|
||||||
|
False,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
DataDirPage.Add('Data Directory:');
|
||||||
|
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||||
|
end;
|
||||||
|
|
||||||
|
function GetDataDir(Param: String): String;
|
||||||
|
begin
|
||||||
|
Result := DataDirPage.Values[0];
|
||||||
|
if Result = '' then
|
||||||
|
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
begin
|
||||||
|
if CurStep = ssPostInstall then
|
||||||
|
begin
|
||||||
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||||
|
var
|
||||||
|
CustomDataDir: String;
|
||||||
|
begin
|
||||||
|
if CurUninstallStep = usPostUninstall then
|
||||||
|
begin
|
||||||
|
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', CustomDataDir) then
|
||||||
|
begin
|
||||||
|
if (CustomDataDir <> '') and DirExists(CustomDataDir) then
|
||||||
|
DelTree(CustomDataDir, True, True, True);
|
||||||
|
RegDeleteKeyIncludingSubkeys(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
!include "MUI2.nsh"
|
||||||
|
!include "LogicLib.nsh"
|
||||||
|
|
||||||
|
Name "LOGAR Server"
|
||||||
|
OutFile "../dist/LOGAR-Server-Setup.exe"
|
||||||
|
InstallDir "$PROGRAMFILES64\LOGAR-Server"
|
||||||
|
RequestExecutionLevel admin
|
||||||
|
|
||||||
|
BrandingText "LOGAR Enterprise SIEM & Log Ingestion"
|
||||||
|
|
||||||
|
VIProductVersion "0.3.1.0"
|
||||||
|
VIAddVersionKey "ProductName" "LOGAR Server"
|
||||||
|
VIAddVersionKey "CompanyName" "LOGAR Security Systems"
|
||||||
|
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR Security Systems"
|
||||||
|
VIAddVersionKey "FileDescription" "LOGAR Cloud Hub & TCP Ingestion Server Setup"
|
||||||
|
VIAddVersionKey "FileVersion" "0.3.1.0"
|
||||||
|
VIAddVersionKey "ProductVersion" "0.3.1.0"
|
||||||
|
|
||||||
|
Var DATADIR
|
||||||
|
|
||||||
|
# --- UI Pages ---
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
|
||||||
|
# Welcome Page
|
||||||
|
!define MUI_WELCOMEPAGE_TITLE "Welcome to LOGAR Server Setup"
|
||||||
|
!define MUI_WELCOMEPAGE_TEXT "LOGAR provides high-performance encrypted log ingestion, temporal analysis, and mTLS client enrollment.\r\n\r\nThis wizard will guide you through installing LOGAR Server and configuring it as an automated Windows service."
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
|
||||||
|
# Directory Page 1: Installation Directory
|
||||||
|
!define MUI_PAGE_HEADER_TEXT "Choose Installation Location"
|
||||||
|
!define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install program binaries."
|
||||||
|
!define MUI_DIRECTORYPAGE_VARIABLE $INSTDIR
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
# Directory Page 2: Data Directory
|
||||||
|
!define MUI_PAGE_HEADER_TEXT "Choose Data & Storage Location"
|
||||||
|
!define MUI_PAGE_HEADER_SUBTEXT "Choose where database, certificates, and configuration files should be stored."
|
||||||
|
!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose the folder where LOGAR Server should store its database (logar_state.db), configuration (server_config.json), and mTLS certificates.$\r$\n$\r$\nDefault is recommended for automatic background service access."
|
||||||
|
!define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data & Storage Folder"
|
||||||
|
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
# Instfiles Page
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
|
# Finish Page
|
||||||
|
!define MUI_FINISHPAGE_TITLE "Installation Complete"
|
||||||
|
!define MUI_FINISHPAGE_TEXT "LOGAR Server has been successfully installed and started as a background Windows service.\r\n\r\nWeb Hub Dashboard: http://localhost:8443\r\nTCP Ingestion Port: 9443"
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
# Uninstaller Pages
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
SetShellVarContext all
|
||||||
|
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section "Install"
|
||||||
|
SetShellVarContext all
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
File "../dist/Server.exe"
|
||||||
|
File "../compilation/nssm.exe"
|
||||||
|
File /r "../dist/runtime"
|
||||||
|
|
||||||
|
CreateDirectory "$DATADIR"
|
||||||
|
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
||||||
|
|
||||||
|
# Install and configure service via NSSM
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Server "$INSTDIR\Server.exe"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppDirectory "$INSTDIR"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$DATADIR\server.log"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStderr "$DATADIR\server_err.log"'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppParameters "--config \"$DATADIR\server_config.json\""'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
|
||||||
|
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
WriteRegStr HKLM "Software\LOGAR\LOGAR-Server" "DataDir" "$DATADIR"
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Uninstall"
|
||||||
|
SetShellVarContext all
|
||||||
|
ReadRegStr $DATADIR HKLM "Software\LOGAR\LOGAR-Server" "DataDir"
|
||||||
|
${If} $DATADIR == ""
|
||||||
|
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server'
|
||||||
|
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm'
|
||||||
|
RMDir /r "$INSTDIR\runtime"
|
||||||
|
RMDir /r "$INSTDIR\certs"
|
||||||
|
Delete "$INSTDIR\server_config.json"
|
||||||
|
Delete "$INSTDIR\logar_state.db"
|
||||||
|
Delete "$INSTDIR\logar_state.db-*"
|
||||||
|
Delete "$INSTDIR\*.log"
|
||||||
|
Delete "$INSTDIR\Server.exe"
|
||||||
|
Delete "$INSTDIR\nssm.exe"
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
RMDir /r "$INSTDIR"
|
||||||
|
RMDir /r "$DATADIR"
|
||||||
|
DeleteRegKey HKLM "Software\LOGAR\LOGAR-Server"
|
||||||
|
SectionEnd
|
||||||
+382
-25
@@ -12,6 +12,7 @@ SRC_DIR = os.path.join(ROOT_DIR, "src")
|
|||||||
DIST_DIR = os.path.join(ROOT_DIR, "dist")
|
DIST_DIR = os.path.join(ROOT_DIR, "dist")
|
||||||
BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
|
BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
|
||||||
|
|
||||||
|
|
||||||
def clean_and_prep():
|
def clean_and_prep():
|
||||||
if os.path.exists(DIST_DIR):
|
if os.path.exists(DIST_DIR):
|
||||||
shutil.rmtree(DIST_DIR)
|
shutil.rmtree(DIST_DIR)
|
||||||
@@ -20,6 +21,22 @@ def clean_and_prep():
|
|||||||
shutil.rmtree(BUILD_TEMP)
|
shutil.rmtree(BUILD_TEMP)
|
||||||
os.makedirs(BUILD_TEMP, exist_ok=True)
|
os.makedirs(BUILD_TEMP, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sha256(filepath):
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
while chunk := f.read(65536):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def write_checksum_file(filename, digest, binary_filename):
|
||||||
|
out_path = os.path.join(DIST_DIR, filename)
|
||||||
|
with open(out_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"{digest} {binary_filename}\n")
|
||||||
|
print(f"[+] Generated checksum file: {filename} ({digest[:16]}...)")
|
||||||
|
|
||||||
|
|
||||||
def build_pyinstaller_binary(script_path, binary_name):
|
def build_pyinstaller_binary(script_path, binary_name):
|
||||||
print(f"[*] Compiling {binary_name} with PyInstaller...")
|
print(f"[*] Compiling {binary_name} with PyInstaller...")
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -38,25 +55,15 @@ def build_pyinstaller_binary(script_path, binary_name):
|
|||||||
raise RuntimeError(f"Failed to build {binary_name}")
|
raise RuntimeError(f"Failed to build {binary_name}")
|
||||||
print(f"[+] Successfully compiled {binary_name}")
|
print(f"[+] Successfully compiled {binary_name}")
|
||||||
|
|
||||||
def calculate_sha256(filepath):
|
|
||||||
h = hashlib.sha256()
|
|
||||||
with open(filepath, "rb") as f:
|
|
||||||
while chunk := f.read(65536):
|
|
||||||
h.update(chunk)
|
|
||||||
return h.hexdigest()
|
|
||||||
|
|
||||||
def write_checksum_file(filename, digest, binary_filename):
|
|
||||||
out_path = os.path.join(DIST_DIR, filename)
|
|
||||||
with open(out_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(f"{digest} {binary_filename}\n")
|
|
||||||
print(f"[+] Generated checksum file: {filename} ({digest[:16]}...)")
|
|
||||||
|
|
||||||
def build_linux_zipapp_fallback():
|
def build_linux_zipapp_fallback():
|
||||||
print("[*] Packaging Linux standalone zipapp fallback binaries...")
|
print("[*] Packaging Linux standalone zipapp fallback binaries...")
|
||||||
# Linux Client zipapp
|
# Linux Client zipapp
|
||||||
app_dir = os.path.join(BUILD_TEMP, "linux_app")
|
app_dir = os.path.join(BUILD_TEMP, "linux_app")
|
||||||
os.makedirs(app_dir, exist_ok=True)
|
os.makedirs(app_dir, exist_ok=True)
|
||||||
shutil.copy(os.path.join(SRC_DIR, "Linux_Client.py"), os.path.join(app_dir, "Linux_Client.py"))
|
for f in os.listdir(SRC_DIR):
|
||||||
|
if f.endswith(".py"):
|
||||||
|
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_dir, f))
|
||||||
client_out = os.path.join(DIST_DIR, "Linux_Client.bin")
|
client_out = os.path.join(DIST_DIR, "Linux_Client.bin")
|
||||||
zipapp.create_archive(
|
zipapp.create_archive(
|
||||||
source=app_dir,
|
source=app_dir,
|
||||||
@@ -67,7 +74,9 @@ def build_linux_zipapp_fallback():
|
|||||||
# Server zipapp
|
# Server zipapp
|
||||||
srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
|
srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
|
||||||
os.makedirs(srv_dir, exist_ok=True)
|
os.makedirs(srv_dir, exist_ok=True)
|
||||||
shutil.copy(os.path.join(SRC_DIR, "Server.py"), os.path.join(srv_dir, "Server.py"))
|
for f in os.listdir(SRC_DIR):
|
||||||
|
if f.endswith(".py"):
|
||||||
|
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(srv_dir, f))
|
||||||
server_out = os.path.join(DIST_DIR, "Server.bin")
|
server_out = os.path.join(DIST_DIR, "Server.bin")
|
||||||
zipapp.create_archive(
|
zipapp.create_archive(
|
||||||
source=srv_dir,
|
source=srv_dir,
|
||||||
@@ -76,16 +85,297 @@ def build_linux_zipapp_fallback():
|
|||||||
main="Server:main"
|
main="Server:main"
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_windows():
|
|
||||||
print("[*] Compiling Windows standalone executables...")
|
|
||||||
win_client_script = os.path.join(SRC_DIR, "Win_Client.py")
|
|
||||||
build_pyinstaller_binary(win_client_script, "Win_Client")
|
|
||||||
|
|
||||||
server_script = os.path.join(SRC_DIR, "Server.py")
|
def prepare_windows_runtime():
|
||||||
build_pyinstaller_binary(server_script, "Server")
|
"""
|
||||||
|
Downloads the official Python 3.11 embeddable 64-bit runtime, configures ._pth,
|
||||||
|
and downloads/unpacks all required dependencies (wheels) for standalone execution.
|
||||||
|
"""
|
||||||
|
runtime_dir = os.path.join(DIST_DIR, "runtime")
|
||||||
|
if os.path.exists(runtime_dir):
|
||||||
|
return runtime_dir
|
||||||
|
|
||||||
|
os.makedirs(runtime_dir, exist_ok=True)
|
||||||
|
site_packages = os.path.join(runtime_dir, "site-packages")
|
||||||
|
os.makedirs(site_packages, exist_ok=True)
|
||||||
|
|
||||||
|
# 1. Download & Extract Python 3.11 embeddable zip
|
||||||
|
embed_zip_name = "python-3.11.9-embed-amd64.zip"
|
||||||
|
embed_zip_path = os.path.join(BUILD_TEMP, embed_zip_name)
|
||||||
|
embed_url = f"https://www.python.org/ftp/python/3.11.9/{embed_zip_name}"
|
||||||
|
|
||||||
|
if not os.path.exists(embed_zip_path):
|
||||||
|
print(f"[*] Downloading Python 3.11 embeddable runtime from {embed_url}...")
|
||||||
|
import urllib.request
|
||||||
|
urllib.request.urlretrieve(embed_url, embed_zip_path)
|
||||||
|
|
||||||
|
import zipfile
|
||||||
|
with zipfile.ZipFile(embed_zip_path, "r") as zf:
|
||||||
|
zf.extractall(runtime_dir)
|
||||||
|
|
||||||
|
# 2. Configure python311._pth
|
||||||
|
pth_file = os.path.join(runtime_dir, "python311._pth")
|
||||||
|
with open(pth_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write("python311.zip\n.\nsite-packages\nsite-packages/win32\nsite-packages/win32/lib\nsite-packages/Pythonwin\nimport site\n")
|
||||||
|
|
||||||
|
# 3. Download and unpack Windows AMD64 dependencies
|
||||||
|
wheels_dir = os.path.join(BUILD_TEMP, "win_wheels")
|
||||||
|
os.makedirs(wheels_dir, exist_ok=True)
|
||||||
|
|
||||||
|
print("[*] Fetching required Windows x64 dependency wheels...")
|
||||||
|
# Build PGPy 0.6.0 wheel from git (supports modern cryptography)
|
||||||
|
try:
|
||||||
|
subprocess.run([
|
||||||
|
sys.executable, "-m", "pip", "wheel", "--no-deps",
|
||||||
|
"git+https://github.com/SecurityInnovation/PGPy.git",
|
||||||
|
"--wheel-dir", wheels_dir
|
||||||
|
], check=False, capture_output=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Download binary wheels for win_amd64 cp311
|
||||||
|
deps = [
|
||||||
|
"cryptography", "fastapi", "uvicorn", "pydantic",
|
||||||
|
"pywin32", "websockets", "pyasn1", "six", "cffi", "pycparser"
|
||||||
|
]
|
||||||
|
pip_cmd = [
|
||||||
|
sys.executable, "-m", "pip", "download",
|
||||||
|
"--only-binary=:all:",
|
||||||
|
"--platform", "win_amd64",
|
||||||
|
"--python-version", "311",
|
||||||
|
"--implementation", "cp",
|
||||||
|
"--abi", "cp311",
|
||||||
|
"--dest", wheels_dir
|
||||||
|
] + deps
|
||||||
|
|
||||||
|
subprocess.run(pip_cmd, check=False, capture_output=True)
|
||||||
|
|
||||||
|
for whl in os.listdir(wheels_dir):
|
||||||
|
if whl.endswith(".whl"):
|
||||||
|
whl_path = os.path.join(wheels_dir, whl)
|
||||||
|
with zipfile.ZipFile(whl_path, "r") as zf:
|
||||||
|
zf.extractall(site_packages)
|
||||||
|
|
||||||
|
# 4. Copy pywin32 system DLLs to runtime root for immediate DLL loader resolution
|
||||||
|
sys32_dir = os.path.join(site_packages, "pywin32_system32")
|
||||||
|
if os.path.exists(sys32_dir):
|
||||||
|
for dll in os.listdir(sys32_dir):
|
||||||
|
if dll.lower().endswith(".dll"):
|
||||||
|
shutil.copy(os.path.join(sys32_dir, dll), runtime_dir)
|
||||||
|
|
||||||
|
print("[+] Successfully assembled standalone Windows Python 3.11 runtime environment.")
|
||||||
|
return runtime_dir
|
||||||
|
|
||||||
|
|
||||||
|
def create_windows_exe_zipapp(script_name, exe_name):
|
||||||
|
"""
|
||||||
|
Cross-compilation helper: Creates a standalone Windows .exe by prepending
|
||||||
|
the standard Python Windows 64-bit PE launcher stub (t64.exe) to a compiled zipapp archive.
|
||||||
|
"""
|
||||||
|
app_temp_dir = os.path.join(BUILD_TEMP, f"win_app_{exe_name}")
|
||||||
|
os.makedirs(app_temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
for f in os.listdir(SRC_DIR):
|
||||||
|
if f.endswith(".py"):
|
||||||
|
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_temp_dir, f))
|
||||||
|
|
||||||
|
module_name = os.path.splitext(script_name)[0]
|
||||||
|
zipapp_temp = os.path.join(BUILD_TEMP, f"{exe_name}.zip")
|
||||||
|
zipapp.create_archive(
|
||||||
|
source=app_temp_dir,
|
||||||
|
target=zipapp_temp,
|
||||||
|
interpreter=r"runtime\python.exe",
|
||||||
|
main=f"{module_name}:main"
|
||||||
|
)
|
||||||
|
|
||||||
|
stub_bytes = b""
|
||||||
|
try:
|
||||||
|
import pip._vendor.distlib.resources as distlib_res
|
||||||
|
finder = distlib_res.finder("pip._vendor.distlib")
|
||||||
|
res = finder.find("t64.exe")
|
||||||
|
if res:
|
||||||
|
stub_bytes = res.bytes
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not stub_bytes:
|
||||||
|
try:
|
||||||
|
import distlib.resources as distlib_res
|
||||||
|
finder = distlib_res.finder("distlib")
|
||||||
|
res = finder.find("t64.exe")
|
||||||
|
if res:
|
||||||
|
stub_bytes = res.bytes
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
out_exe_path = os.path.join(DIST_DIR, f"{exe_name}.exe")
|
||||||
|
with open(out_exe_path, "wb") as out_f:
|
||||||
|
if stub_bytes:
|
||||||
|
out_f.write(stub_bytes)
|
||||||
|
with open(zipapp_temp, "rb") as in_f:
|
||||||
|
out_f.write(in_f.read())
|
||||||
|
print(f"[+] Successfully cross-compiled Windows PE executable: {exe_name}.exe (PE Stub attached: {bool(stub_bytes)})")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_makensis():
|
||||||
|
"""Finds or attempts to install makensis on the current system."""
|
||||||
|
for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis",
|
||||||
|
r"C:\Program Files (x86)\NSIS\makensis.exe",
|
||||||
|
r"C:\Program Files\NSIS\makensis.exe"]:
|
||||||
|
p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd
|
||||||
|
if p and (os.path.exists(p) or shutil.which(p)):
|
||||||
|
return p
|
||||||
|
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
print("[*] Attempting to install 'nsis' package dynamically...")
|
||||||
|
sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else []
|
||||||
|
if shutil.which("apt-get"):
|
||||||
|
try:
|
||||||
|
subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False)
|
||||||
|
subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False)
|
||||||
|
subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", "nsis", "nsis-common"], check=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Warning: Dynamic apt install failed: {e}")
|
||||||
|
elif shutil.which("apk"):
|
||||||
|
try:
|
||||||
|
subprocess.run(sudo + ["apk", "add", "--no-cache", "nsis"], check=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif shutil.which("dnf"):
|
||||||
|
try:
|
||||||
|
subprocess.run(sudo + ["dnf", "install", "-y", "mingw32-nsis", "nsis"], check=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis"]:
|
||||||
|
p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd
|
||||||
|
if p and (os.path.exists(p) or shutil.which(p)):
|
||||||
|
return p
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_libpython():
|
||||||
|
"""Attempts to install libpython shared library on Debian/Ubuntu for PyInstaller."""
|
||||||
|
if platform.system() == "Linux" and shutil.which("apt-get"):
|
||||||
|
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||||
|
sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else []
|
||||||
|
try:
|
||||||
|
subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False)
|
||||||
|
subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False)
|
||||||
|
subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", f"libpython{py_ver}", "python3-dev", f"python{py_ver}-dev", "libpython3-dev"], check=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def compile_nsis_installers():
|
||||||
|
"""Compiles NSIS installers if makensis is available (native Linux/Windows)."""
|
||||||
|
makensis_bin = ensure_makensis()
|
||||||
|
if not makensis_bin:
|
||||||
|
print("[!] Note: NSIS compiler (makensis) not found in PATH or standard directories.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
makensis_cmd = [makensis_bin]
|
||||||
|
compilation_dir = os.path.join(ROOT_DIR, "compilation")
|
||||||
|
success = True
|
||||||
|
for nsi in ["installer_client.nsi", "installer_server.nsi"]:
|
||||||
|
nsi_path = os.path.join(compilation_dir, nsi)
|
||||||
|
if os.path.exists(nsi_path):
|
||||||
|
print(f"[*] Compiling NSIS script: {nsi} via {makensis_cmd[0]}...")
|
||||||
|
try:
|
||||||
|
subprocess.run(makensis_cmd + [nsi_path], cwd=compilation_dir, check=True)
|
||||||
|
print(f"[+] Successfully compiled NSIS installer for {nsi}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Warning: Failed to compile {nsi} with {makensis_cmd}: {e}")
|
||||||
|
success = False
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
def compile_inno_setup_installers():
|
||||||
|
"""Compiles Inno Setup installers if iscc or innosetup wrapper is available."""
|
||||||
|
iscc_cmd = None
|
||||||
|
user_iscc = os.path.expanduser(r"~\AppData\Local\Programs\Inno Setup 6\ISCC.exe")
|
||||||
|
if shutil.which("iscc"):
|
||||||
|
iscc_cmd = ["iscc"]
|
||||||
|
elif shutil.which("innosetup"):
|
||||||
|
iscc_cmd = ["innosetup"]
|
||||||
|
elif os.path.exists(user_iscc):
|
||||||
|
iscc_cmd = [user_iscc]
|
||||||
|
elif os.path.exists("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"):
|
||||||
|
iscc_cmd = ["C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"]
|
||||||
|
elif os.path.exists("C:\\Program Files\\Inno Setup 6\\ISCC.exe"):
|
||||||
|
iscc_cmd = ["C:\\Program Files\\Inno Setup 6\\ISCC.exe"]
|
||||||
|
elif shutil.which("wine") and os.path.exists("/opt/innosetup/app/ISCC.exe"):
|
||||||
|
iscc_cmd = ["wine", "/opt/innosetup/app/ISCC.exe"]
|
||||||
|
elif shutil.which("wine"):
|
||||||
|
wine_iscc = os.path.expanduser("~/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe")
|
||||||
|
if os.path.exists(wine_iscc):
|
||||||
|
iscc_cmd = ["wine", wine_iscc]
|
||||||
|
|
||||||
|
if not iscc_cmd:
|
||||||
|
print("[!] Note: Inno Setup compiler (iscc / innosetup) not available on host. Skipping installer .exe creation.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
for iss in ["installer_client.iss", "installer_server.iss"]:
|
||||||
|
iss_path = os.path.join(ROOT_DIR, "compilation", iss)
|
||||||
|
if os.path.exists(iss_path):
|
||||||
|
print(f"[*] Compiling Inno Setup script: {iss}...")
|
||||||
|
try:
|
||||||
|
subprocess.run(iscc_cmd + [iss_path], check=True)
|
||||||
|
print(f"[+] Successfully compiled {iss}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Warning: Failed to compile {iss} with {iscc_cmd}: {e}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def compile_windows_installers():
|
||||||
|
if not compile_nsis_installers():
|
||||||
|
compile_inno_setup_installers()
|
||||||
|
|
||||||
|
|
||||||
|
def build_windows():
|
||||||
|
print("[*] Packaging self-contained Windows standalone executables...")
|
||||||
|
prepare_windows_runtime()
|
||||||
|
create_windows_exe_zipapp("Win_Client.py", "Win_Client")
|
||||||
|
create_windows_exe_zipapp("Server.py", "Server")
|
||||||
|
|
||||||
|
compile_windows_installers()
|
||||||
|
|
||||||
client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
|
client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
|
||||||
server_bin = os.path.join(DIST_DIR, "Server.exe")
|
server_bin = os.path.join(DIST_DIR, "Server.exe")
|
||||||
|
runtime_dir = os.path.join(DIST_DIR, "runtime")
|
||||||
|
|
||||||
|
# Windows Zip Bundles
|
||||||
|
import zipfile
|
||||||
|
if os.path.exists(client_bin):
|
||||||
|
client_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Client.zip")
|
||||||
|
with zipfile.ZipFile(client_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
zf.write(client_bin, arcname="Win_Client.exe")
|
||||||
|
sample_cfg = os.path.join(ROOT_DIR, "out", "win_client", "client_config.sample.json")
|
||||||
|
if os.path.exists(sample_cfg):
|
||||||
|
zf.write(sample_cfg, arcname="client_config.sample.json")
|
||||||
|
if os.path.exists(runtime_dir):
|
||||||
|
for root, _, files in os.walk(runtime_dir):
|
||||||
|
for file in files:
|
||||||
|
full_p = os.path.join(root, file)
|
||||||
|
rel_p = os.path.relpath(full_p, DIST_DIR)
|
||||||
|
zf.write(full_p, arcname=rel_p)
|
||||||
|
print(f"[+] Packaged Windows Client Zip: {os.path.basename(client_zip)}")
|
||||||
|
|
||||||
|
if os.path.exists(server_bin):
|
||||||
|
server_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Server.zip")
|
||||||
|
with zipfile.ZipFile(server_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
zf.write(server_bin, arcname="Server.exe")
|
||||||
|
sample_cfg = os.path.join(ROOT_DIR, "server_config.sample.json")
|
||||||
|
if os.path.exists(sample_cfg):
|
||||||
|
zf.write(sample_cfg, arcname="server_config.sample.json")
|
||||||
|
if os.path.exists(runtime_dir):
|
||||||
|
for root, _, files in os.walk(runtime_dir):
|
||||||
|
for file in files:
|
||||||
|
full_p = os.path.join(root, file)
|
||||||
|
rel_p = os.path.relpath(full_p, DIST_DIR)
|
||||||
|
zf.write(full_p, arcname=rel_p)
|
||||||
|
print(f"[+] Packaged Windows Server Zip: {os.path.basename(server_zip)}")
|
||||||
|
|
||||||
sums = []
|
sums = []
|
||||||
if os.path.exists(client_bin):
|
if os.path.exists(client_bin):
|
||||||
@@ -103,20 +393,33 @@ def build_windows():
|
|||||||
else:
|
else:
|
||||||
print(f"[!] Warning: Expected {server_bin} was not found.")
|
print(f"[!] Warning: Expected {server_bin} was not found.")
|
||||||
|
|
||||||
|
# Inno Setup output checksums if present
|
||||||
|
for inst in ["LOGAR-Client-Setup.exe", "LOGAR-Server-Setup.exe", "LOGAR-Windows-Client.zip", "LOGAR-Windows-Server.zip"]:
|
||||||
|
inst_path = os.path.join(DIST_DIR, inst)
|
||||||
|
if os.path.exists(inst_path):
|
||||||
|
inst_hash = calculate_sha256(inst_path)
|
||||||
|
sums.append(f"{inst_hash} {inst}")
|
||||||
|
|
||||||
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_windows.txt")
|
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_windows.txt")
|
||||||
with open(sums_path, "w", encoding="utf-8") as f:
|
with open(sums_path, "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(sums) + "\n")
|
f.write("\n".join(sums) + "\n")
|
||||||
|
|
||||||
|
|
||||||
def build_linux():
|
def build_linux():
|
||||||
print("[*] Compiling Linux standalone binaries...")
|
print("[*] Compiling Linux standalone binaries and packaging installers...")
|
||||||
is_linux_host = platform.system() == "Linux"
|
is_linux_host = platform.system() == "Linux"
|
||||||
|
|
||||||
if is_linux_host:
|
if is_linux_host:
|
||||||
|
ensure_libpython()
|
||||||
|
try:
|
||||||
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py")
|
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py")
|
||||||
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
|
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
|
||||||
|
|
||||||
server_script = os.path.join(SRC_DIR, "Server.py")
|
server_script = os.path.join(SRC_DIR, "Server.py")
|
||||||
build_pyinstaller_binary(server_script, "Server.bin")
|
build_pyinstaller_binary(server_script, "Server.bin")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] PyInstaller compilation encountered an issue ({e}). Falling back to standalone zipapp packaging...")
|
||||||
|
build_linux_zipapp_fallback()
|
||||||
|
|
||||||
# Normalize extensions in case PyInstaller dropped .bin
|
# Normalize extensions in case PyInstaller dropped .bin
|
||||||
for name in ["Linux_Client", "Server"]:
|
for name in ["Linux_Client", "Server"]:
|
||||||
@@ -137,9 +440,37 @@ def build_linux():
|
|||||||
print("[!] Note: Host platform is not Linux. Generating executable zipapps for Linux target.")
|
print("[!] Note: Host platform is not Linux. Generating executable zipapps for Linux target.")
|
||||||
build_linux_zipapp_fallback()
|
build_linux_zipapp_fallback()
|
||||||
|
|
||||||
|
# Copy Linux installation scripts directly to dist
|
||||||
|
for sh in ["install_linux_client.sh", "install_linux_server.sh"]:
|
||||||
|
sh_src = os.path.join(ROOT_DIR, "compilation", sh)
|
||||||
|
if os.path.exists(sh_src):
|
||||||
|
sh_dst = os.path.join(DIST_DIR, sh)
|
||||||
|
shutil.copy(sh_src, sh_dst)
|
||||||
|
print(f"[+] Copied Linux installer script: {sh}")
|
||||||
|
|
||||||
|
# Package Linux tarballs
|
||||||
|
import tarfile
|
||||||
client_bin = os.path.join(DIST_DIR, "Linux_Client.bin")
|
client_bin = os.path.join(DIST_DIR, "Linux_Client.bin")
|
||||||
server_bin = os.path.join(DIST_DIR, "Server.bin")
|
server_bin = os.path.join(DIST_DIR, "Server.bin")
|
||||||
|
|
||||||
|
if os.path.exists(client_bin):
|
||||||
|
client_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Client.tar.gz")
|
||||||
|
with tarfile.open(client_tar, "w:gz") as tf:
|
||||||
|
tf.add(client_bin, arcname="Linux_Client.bin")
|
||||||
|
client_sh = os.path.join(DIST_DIR, "install_linux_client.sh")
|
||||||
|
if os.path.exists(client_sh):
|
||||||
|
tf.add(client_sh, arcname="install_linux_client.sh")
|
||||||
|
print(f"[+] Packaged Linux Client Tarball: {os.path.basename(client_tar)}")
|
||||||
|
|
||||||
|
if os.path.exists(server_bin):
|
||||||
|
server_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Server.tar.gz")
|
||||||
|
with tarfile.open(server_tar, "w:gz") as tf:
|
||||||
|
tf.add(server_bin, arcname="Server.bin")
|
||||||
|
server_sh = os.path.join(DIST_DIR, "install_linux_server.sh")
|
||||||
|
if os.path.exists(server_sh):
|
||||||
|
tf.add(server_sh, arcname="install_linux_server.sh")
|
||||||
|
print(f"[+] Packaged Linux Server Tarball: {os.path.basename(server_tar)}")
|
||||||
|
|
||||||
sums = []
|
sums = []
|
||||||
if os.path.exists(client_bin):
|
if os.path.exists(client_bin):
|
||||||
client_hash = calculate_sha256(client_bin)
|
client_hash = calculate_sha256(client_bin)
|
||||||
@@ -156,16 +487,39 @@ def build_linux():
|
|||||||
else:
|
else:
|
||||||
print(f"[!] Warning: Expected {server_bin} was not found.")
|
print(f"[!] Warning: Expected {server_bin} was not found.")
|
||||||
|
|
||||||
|
for item in ["install_linux_client.sh", "install_linux_server.sh", "LOGAR-Linux-Client.tar.gz", "LOGAR-Linux-Server.tar.gz"]:
|
||||||
|
item_path = os.path.join(DIST_DIR, item)
|
||||||
|
if os.path.exists(item_path):
|
||||||
|
item_hash = calculate_sha256(item_path)
|
||||||
|
sums.append(f"{item_hash} {item}")
|
||||||
|
|
||||||
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_linux.txt")
|
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_linux.txt")
|
||||||
with open(sums_path, "w", encoding="utf-8") as f:
|
with open(sums_path, "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(sums) + "\n")
|
f.write("\n".join(sums) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def build_all():
|
||||||
|
print("[*] Building standalone distribution artifacts for both Linux and Windows...")
|
||||||
|
build_linux()
|
||||||
|
build_windows()
|
||||||
|
|
||||||
|
# Consolidated master checksums file
|
||||||
|
all_sums = []
|
||||||
|
for f in sorted(os.listdir(DIST_DIR)):
|
||||||
|
fpath = os.path.join(DIST_DIR, f)
|
||||||
|
if os.path.isfile(fpath) and not f.endswith(".txt") and not "sha256sum" in f:
|
||||||
|
h = calculate_sha256(fpath)
|
||||||
|
all_sums.append(f"{h} {f}")
|
||||||
|
with open(os.path.join(DIST_DIR, "SHA256SUMS.txt"), "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(all_sums) + "\n")
|
||||||
|
|
||||||
|
|
||||||
def main(target=None):
|
def main(target=None):
|
||||||
if target is None:
|
if target is None:
|
||||||
parser = argparse.ArgumentParser(description="LOGAR Standalone Binary Compiler & Packager")
|
parser = argparse.ArgumentParser(description="LOGAR Standalone Binary Compiler & Packager")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--target", "-t",
|
"--target", "-t",
|
||||||
choices=["windows", "win", "linux", "auto"],
|
choices=["all", "windows", "win", "linux", "auto"],
|
||||||
default="auto",
|
default="auto",
|
||||||
help="Target platform to compile binaries for (default: auto-detect)"
|
help="Target platform to compile binaries for (default: auto-detect)"
|
||||||
)
|
)
|
||||||
@@ -173,19 +527,21 @@ def main(target=None):
|
|||||||
target = args.target
|
target = args.target
|
||||||
|
|
||||||
if target == "auto":
|
if target == "auto":
|
||||||
target = "windows" if platform.system() == "Windows" else "linux"
|
target = "all"
|
||||||
elif target == "win":
|
elif target == "win":
|
||||||
target = "windows"
|
target = "windows"
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(" LOGAR Binary Packaging")
|
print(" LOGAR Binary Packaging & Cross-Compilation")
|
||||||
print(f" Host Platform: {platform.system()} ({platform.machine()})")
|
print(f" Host Platform: {platform.system()} ({platform.machine()})")
|
||||||
print(f" Target Platform: {target.upper()}")
|
print(f" Target Platform: {target.upper()}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
clean_and_prep()
|
clean_and_prep()
|
||||||
|
|
||||||
if target == "windows":
|
if target == "all":
|
||||||
|
build_all()
|
||||||
|
elif target == "windows":
|
||||||
build_windows()
|
build_windows()
|
||||||
elif target == "linux":
|
elif target == "linux":
|
||||||
build_linux()
|
build_linux()
|
||||||
@@ -202,5 +558,6 @@ def main(target=None):
|
|||||||
print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)")
|
print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ fastapi>=0.110.0
|
|||||||
uvicorn>=0.28.0
|
uvicorn>=0.28.0
|
||||||
websockets>=11.0.0
|
websockets>=11.0.0
|
||||||
pydantic>=2.6.0
|
pydantic>=2.6.0
|
||||||
|
distlib>=0.3.8
|
||||||
pywin32>=306; sys_platform == "win32"
|
pywin32>=306; sys_platform == "win32"
|
||||||
|
|||||||
@@ -13,9 +13,21 @@ import time
|
|||||||
DEFAULT_GITEA_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.eibl.tech")
|
DEFAULT_GITEA_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.eibl.tech")
|
||||||
DEFAULT_REPO = os.environ.get("GITEA_REPOSITORY", "me0nline/LOGAR")
|
DEFAULT_REPO = os.environ.get("GITEA_REPOSITORY", "me0nline/LOGAR")
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def get_auth_header(token):
|
||||||
|
if not token:
|
||||||
|
return ""
|
||||||
|
if token.startswith("Basic ") or token.startswith("token ") or token.startswith("Bearer "):
|
||||||
|
return token
|
||||||
|
if ":" in token:
|
||||||
|
b64 = base64.b64encode(token.encode("utf-8")).decode("ascii")
|
||||||
|
return f"Basic {b64}"
|
||||||
|
return f"token {token}"
|
||||||
|
|
||||||
def get_existing_assets(base_url, repo, release_id, token):
|
def get_existing_assets(base_url, repo, release_id, token):
|
||||||
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
|
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
|
||||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}", "Accept": "application/json"})
|
req = urllib.request.Request(url, headers={"Authorization": get_auth_header(token), "Accept": "application/json"})
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req) as resp:
|
with urllib.request.urlopen(req) as resp:
|
||||||
return json.loads(resp.read().decode("utf-8"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
@@ -24,7 +36,7 @@ def get_existing_assets(base_url, repo, release_id, token):
|
|||||||
|
|
||||||
def delete_asset(base_url, repo, release_id, asset_id, token):
|
def delete_asset(base_url, repo, release_id, asset_id, token):
|
||||||
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}"
|
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}"
|
||||||
req = urllib.request.Request(url, method="DELETE", headers={"Authorization": f"token {token}"})
|
req = urllib.request.Request(url, method="DELETE", headers={"Authorization": get_auth_header(token)})
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req) as resp:
|
with urllib.request.urlopen(req) as resp:
|
||||||
pass
|
pass
|
||||||
@@ -47,7 +59,7 @@ def upload_file_to_release(base_url, repo, release_id, token, file_path, max_ret
|
|||||||
|
|
||||||
for attempt in range(1, max_retries + 1):
|
for attempt in range(1, max_retries + 1):
|
||||||
req = urllib.request.Request(url, data=file_bytes, method="POST")
|
req = urllib.request.Request(url, data=file_bytes, method="POST")
|
||||||
req.add_header("Authorization", f"token {token}")
|
req.add_header("Authorization", get_auth_header(token))
|
||||||
req.add_header("Content-Type", "application/octet-stream")
|
req.add_header("Content-Type", "application/octet-stream")
|
||||||
req.add_header("Accept", "application/json")
|
req.add_header("Accept", "application/json")
|
||||||
|
|
||||||
@@ -73,14 +85,14 @@ def upload_file_to_release(base_url, repo, release_id, token, file_path, max_ret
|
|||||||
def create_or_get_release(base_url, repo, tag, token, title=None, notes=None):
|
def create_or_get_release(base_url, repo, tag, token, title=None, notes=None):
|
||||||
url = f"{base_url}/api/v1/repos/{repo}/releases"
|
url = f"{base_url}/api/v1/repos/{repo}/releases"
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"token {token}",
|
"Authorization": get_auth_header(token),
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json"
|
"Accept": "application/json"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if release exists
|
# Check if release exists
|
||||||
check_url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{urllib.parse.quote(tag)}"
|
check_url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{urllib.parse.quote(tag)}"
|
||||||
check_req = urllib.request.Request(check_url, headers={"Authorization": f"token {token}"})
|
check_req = urllib.request.Request(check_url, headers={"Authorization": get_auth_header(token)})
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(check_req) as resp:
|
with urllib.request.urlopen(check_req) as resp:
|
||||||
existing = json.loads(resp.read().decode("utf-8"))
|
existing = json.loads(resp.read().decode("utf-8"))
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Gitea Instance URL
|
||||||
|
GITEA_INSTANCE_URL=https://gitea.eibl.tech
|
||||||
|
|
||||||
|
# Gitea Runner Registration Token (From Gitea Settings -> Actions -> Runners -> Create Runner)
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN=your_runner_registration_token_here
|
||||||
|
|
||||||
|
# Runner Configuration
|
||||||
|
GITEA_RUNNER_NAME=logar-build-runner
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
LABEL maintainer="me0nline <me0nline@eibl.tech>"
|
||||||
|
LABEL description="LOGAR CI/CD, Binary Packaging & Release Runner"
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
|
LC_ALL=C.UTF-8
|
||||||
|
|
||||||
|
# ── 1. Core System Tools, Python Dev Headers & NSIS Compiler ───────────────────
|
||||||
|
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
|
||||||
|
# Basic utilities
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
tar \
|
||||||
|
gzip \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
# Native compilation toolchains
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
make \
|
||||||
|
# Python 3 & dynamic shared libraries (required for PyInstaller standalone binaries)
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
python3-dev \
|
||||||
|
libpython3-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libssl-dev \
|
||||||
|
# NSIS (for compiling Windows Setup executables on Linux)
|
||||||
|
nsis \
|
||||||
|
nsis-common \
|
||||||
|
nsis-pluginapi \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# ── 2. Node.js 20 LTS (required for Act / Gitea Actions runners) ───────────────
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y -qq nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# ── 3. Pre-installed Python Build & Application Dependencies ───────────────────
|
||||||
|
RUN pip3 install --no-cache-dir \
|
||||||
|
# Binary compiler & PE packaging tools
|
||||||
|
pyinstaller \
|
||||||
|
distlib \
|
||||||
|
cryptography \
|
||||||
|
pefile \
|
||||||
|
cffi \
|
||||||
|
# LOGAR core dependencies
|
||||||
|
pgpy \
|
||||||
|
fastapi \
|
||||||
|
uvicorn \
|
||||||
|
pydantic \
|
||||||
|
requests \
|
||||||
|
# Code quality / linter
|
||||||
|
flake8
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
CMD ["/bin/bash"]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# LOGAR Custom CI/CD Runner
|
||||||
|
|
||||||
|
This directory contains the Docker configuration for the custom **LOGAR Build & Release Runner** (`gitea.eibl.tech/me0nline/logar-runner:latest`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Included Toolchains & Dependencies
|
||||||
|
|
||||||
|
- **Operating System**: Ubuntu 22.04 LTS
|
||||||
|
- **Python Development**:
|
||||||
|
- `python3`, `python3-dev`, `libpython3-dev` (shared libraries for native ELF compilation)
|
||||||
|
- `python3-pip`, `python3-venv`, `python3-setuptools`, `python3-wheel`
|
||||||
|
- **Native NSIS Cross-Compiler**:
|
||||||
|
- `nsis`, `nsis-common`, `nsis-pluginapi` (`makensis` CLI for compiling Windows `.exe` installers on Linux)
|
||||||
|
- **Build Essentials**:
|
||||||
|
- `gcc`, `make`, `binutils`, `build-essential`, `libffi-dev`, `libssl-dev`
|
||||||
|
- **Pre-installed Python Packages**:
|
||||||
|
- `pyinstaller`, `distlib`, `cryptography`, `pefile`, `pgpy`, `fastapi`, `uvicorn`, `pydantic`, `requests`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Integration
|
||||||
|
|
||||||
|
You can run CI/CD jobs directly inside this container image in `.gitea/workflows/`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build & Release Standalone Binaries
|
||||||
|
runs-on: logar
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: python3 compilation/package_dist.py --target all
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building & Pushing the Runner
|
||||||
|
|
||||||
|
Trigger the automated workflow [`.gitea/workflows/build-runner.yml`](../.gitea/workflows/build-runner.yml) via the Gitea Actions UI (Actions → Build and Push LOGAR Runner → Run Workflow) or push changes to the `runner/` folder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploying the Runner Stack via Portainer
|
||||||
|
|
||||||
|
1. In Gitea, navigate to **Site Administration** (or Repo **Settings**) → **Actions** → **Runners** → **Create Runner**. Copy the registration token.
|
||||||
|
2. In Portainer, navigate to **Stacks** → **Add stack**.
|
||||||
|
3. Set the name to `logar-act-runner`.
|
||||||
|
4. Paste the contents of [`runner/docker-compose.yml`](docker-compose.yml) into the Web editor (or link to this Git repository).
|
||||||
|
5. In Environment variables, set:
|
||||||
|
- `GITEA_RUNNER_REGISTRATION_TOKEN`: `<your-gitea-runner-token>`
|
||||||
|
6. Click **Deploy the stack**.
|
||||||
|
|
||||||
|
The runner will automatically register with Gitea under the dedicated label `logar`, executing jobs inside the pre-configured `gitea.eibl.tech/me0nline/logar-runner:latest` container.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
logar-runner:
|
||||||
|
image: gitea/act_runner:latest
|
||||||
|
container_name: logar-act-runner
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- GITEA_INSTANCE_URL=${GITEA_INSTANCE_URL:-https://gitea.eibl.tech}
|
||||||
|
- GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||||
|
- GITEA_RUNNER_NAME=${GITEA_RUNNER_NAME:-logar-build-runner}
|
||||||
|
- GITEA_RUNNER_LABELS=logar:docker://gitea.eibl.tech/me0nline/logar-runner:latest
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- logar_runner_data:/data
|
||||||
|
networks:
|
||||||
|
- runner-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logar_runner_data:
|
||||||
|
name: logar_runner_data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
runner-net:
|
||||||
|
name: logar_runner_net
|
||||||
@@ -6,7 +6,6 @@ Repo: https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
|
||||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|||||||
+3
-1
@@ -9,7 +9,7 @@ import subprocess
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional
|
||||||
|
|
||||||
# Suppress cryptography / pgpy deprecation notices
|
# Suppress cryptography / pgpy deprecation notices
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
@@ -334,6 +334,8 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
|
|||||||
hub_url = None
|
hub_url = None
|
||||||
if enrollment_secret:
|
if enrollment_secret:
|
||||||
hermes_host = config.get("hermes_host", server_host)
|
hermes_host = config.get("hermes_host", server_host)
|
||||||
|
if hermes_host in ("0.0.0.0", "::", None):
|
||||||
|
hermes_host = server_host if server_host not in ("0.0.0.0", "::", None) else "127.0.0.1"
|
||||||
hermes_port = config.get("hermes_port", 8443)
|
hermes_port = config.get("hermes_port", 8443)
|
||||||
hub_url = f"http://{hermes_host}:{hermes_port}"
|
hub_url = f"http://{hermes_host}:{hermes_port}"
|
||||||
try:
|
try:
|
||||||
|
|||||||
+1747
-501
File diff suppressed because it is too large
Load Diff
+87
-4
@@ -8,7 +8,7 @@ import argparse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional
|
||||||
|
|
||||||
# Suppress cryptography / pgpy deprecation notices
|
# Suppress cryptography / pgpy deprecation notices
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
@@ -115,10 +115,80 @@ def get_tls_socket(hub_host: str, hub_port: int, cert_dir: str):
|
|||||||
return ctx.wrap_socket(raw_sock, server_hostname=hub_host)
|
return ctx.wrap_socket(raw_sock, server_hostname=hub_host)
|
||||||
|
|
||||||
|
|
||||||
|
def is_path_writable(filepath: str) -> bool:
|
||||||
|
"""Checks if a file or directory path is actually writable by the current process."""
|
||||||
|
try:
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
if os.path.isdir(filepath):
|
||||||
|
test_f = os.path.join(filepath, f".perm_{os.getpid()}")
|
||||||
|
with open(test_f, "w") as f:
|
||||||
|
f.write("1")
|
||||||
|
os.remove(test_f)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
with open(filepath, "a") as f:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
parent = os.path.dirname(os.path.abspath(filepath)) or "."
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
test_f = os.path.join(parent, f".perm_{os.getpid()}")
|
||||||
|
with open(test_f, "w") as f:
|
||||||
|
f.write("1")
|
||||||
|
os.remove(test_f)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_data_dir(app_name: str = "LOGAR") -> str:
|
||||||
|
"""Returns a suitable writable application data directory."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# 1. ProgramData (C:\ProgramData\LOGAR)
|
||||||
|
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
|
||||||
|
target_prog = os.path.join(prog_data, app_name)
|
||||||
|
if is_path_writable(target_prog):
|
||||||
|
return target_prog
|
||||||
|
|
||||||
|
# 2. LocalAppData (C:\Users\<user>\AppData\Local\LOGAR)
|
||||||
|
local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
|
||||||
|
target_local = os.path.join(local_app, app_name)
|
||||||
|
if is_path_writable(target_local):
|
||||||
|
return target_local
|
||||||
|
|
||||||
|
# 3. Temp directory (%TEMP%\LOGAR)
|
||||||
|
temp_dir = os.path.join(os.environ.get("TEMP", r"C:\Windows\Temp"), app_name)
|
||||||
|
if is_path_writable(temp_dir):
|
||||||
|
return temp_dir
|
||||||
|
|
||||||
|
cwd = os.getcwd()
|
||||||
|
if is_path_writable(cwd):
|
||||||
|
return cwd
|
||||||
|
return os.path.expanduser("~")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_writable_path(path: str, app_name: str = "LOGAR", default_filename: str = "client_state.json") -> str:
|
||||||
|
"""Resolves a file or directory path, ensuring it is writable or falling back to a writable data directory."""
|
||||||
|
if not path:
|
||||||
|
path = default_filename
|
||||||
|
|
||||||
|
if os.path.isabs(path) and is_path_writable(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()):
|
||||||
|
return os.path.abspath(path)
|
||||||
|
|
||||||
|
data_dir = get_default_data_dir(app_name)
|
||||||
|
fname = os.path.basename(path)
|
||||||
|
return os.path.join(data_dir, fname)
|
||||||
|
|
||||||
|
|
||||||
def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str:
|
def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str:
|
||||||
if custom_state_path:
|
if custom_state_path:
|
||||||
return custom_state_path
|
return custom_state_path
|
||||||
config_dir = os.path.dirname(os.path.abspath(config_path))
|
config_dir = os.path.dirname(os.path.abspath(config_path))
|
||||||
|
if sys.platform == "win32" and ("Program Files" in config_dir or not is_path_writable(config_dir)):
|
||||||
|
return os.path.join(get_default_data_dir("LOGAR"), STATE_FILE_NAME)
|
||||||
return os.path.join(config_dir, STATE_FILE_NAME)
|
return os.path.join(config_dir, STATE_FILE_NAME)
|
||||||
|
|
||||||
|
|
||||||
@@ -135,6 +205,7 @@ def load_state(state_path: str) -> dict:
|
|||||||
|
|
||||||
def save_state(state_path: str, state: dict):
|
def save_state(state_path: str, state: dict):
|
||||||
try:
|
try:
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(state_path)), exist_ok=True)
|
||||||
temp_path = f"{state_path}.tmp"
|
temp_path = f"{state_path}.tmp"
|
||||||
with open(temp_path, "w", encoding="utf-8") as f:
|
with open(temp_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(state, f, indent=2)
|
json.dump(state, f, indent=2)
|
||||||
@@ -154,13 +225,23 @@ def commit_state(state: dict, state_path: str):
|
|||||||
|
|
||||||
|
|
||||||
def load_config(config_path: str = CONFIG_FILE_NAME):
|
def load_config(config_path: str = CONFIG_FILE_NAME):
|
||||||
if not os.path.exists(config_path):
|
actual_config_path = config_path
|
||||||
|
if not os.path.exists(actual_config_path):
|
||||||
|
data_dir_cfg = os.path.join(get_default_data_dir("LOGAR"), os.path.basename(config_path))
|
||||||
|
if os.path.exists(data_dir_cfg):
|
||||||
|
actual_config_path = data_dir_cfg
|
||||||
|
|
||||||
|
if not os.path.exists(actual_config_path):
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Client configuration file not found at: {config_path}\n"
|
f"Client configuration file not found at: {config_path}\n"
|
||||||
f"Generate one from the server using: python Server.py --create-client-config --client-out {config_path}"
|
f"Generate one from the server using: python Server.py --create-client-config --client-out {config_path}"
|
||||||
)
|
)
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
with open(actual_config_path, "r", encoding="utf-8") as f:
|
||||||
return json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
|
if "cert_dir" in config:
|
||||||
|
config["cert_dir"] = resolve_writable_path(config["cert_dir"], "LOGAR", "certs")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_machine_identifier() -> str:
|
def get_machine_identifier() -> str:
|
||||||
@@ -301,6 +382,8 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
|
|||||||
hub_url = None
|
hub_url = None
|
||||||
if enrollment_secret:
|
if enrollment_secret:
|
||||||
hermes_host = config.get("hermes_host", server_host)
|
hermes_host = config.get("hermes_host", server_host)
|
||||||
|
if hermes_host in ("0.0.0.0", "::", None):
|
||||||
|
hermes_host = server_host if server_host not in ("0.0.0.0", "::", None) else "127.0.0.1"
|
||||||
hermes_port = config.get("hermes_port", 8443)
|
hermes_port = config.get("hermes_port", 8443)
|
||||||
hub_url = f"http://{hermes_host}:{hermes_port}"
|
hub_url = f"http://{hermes_host}:{hermes_port}"
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -662,6 +662,7 @@ def enroll_and_sync_license_ws(
|
|||||||
hostname: str = "",
|
hostname: str = "",
|
||||||
comm_cert_pem: Optional[str] = None,
|
comm_cert_pem: Optional[str] = None,
|
||||||
master_root_ca_pem: Optional[str] = None,
|
master_root_ca_pem: Optional[str] = None,
|
||||||
|
seats_taken: Optional[int] = None,
|
||||||
timeout: int = 15,
|
timeout: int = 15,
|
||||||
verify_tls: bool = True
|
verify_tls: bool = True
|
||||||
) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
|
) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
|
||||||
@@ -731,7 +732,7 @@ def enroll_and_sync_license_ws(
|
|||||||
ssl_context.check_hostname = False
|
ssl_context.check_hostname = False
|
||||||
ssl_context.verify_mode = ssl.CERT_NONE
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
elif master_root_ca_pem:
|
elif master_root_ca_pem:
|
||||||
ssl_context.load_verify_locations(cdata=master_root_ca_pem)
|
ssl_context.load_verify_locations(cadata=master_root_ca_pem)
|
||||||
else:
|
else:
|
||||||
upstream_path = os.path.join(cert_dir, "upstream_ca.crt")
|
upstream_path = os.path.join(cert_dir, "upstream_ca.crt")
|
||||||
if os.path.exists(upstream_path):
|
if os.path.exists(upstream_path):
|
||||||
@@ -752,6 +753,8 @@ def enroll_and_sync_license_ws(
|
|||||||
"server_fingerprint": server_fingerprint,
|
"server_fingerprint": server_fingerprint,
|
||||||
"hostname": hostname
|
"hostname": hostname
|
||||||
}
|
}
|
||||||
|
if seats_taken is not None:
|
||||||
|
payload["seats_taken"] = seats_taken
|
||||||
ws.send(json.dumps(payload))
|
ws.send(json.dumps(payload))
|
||||||
raw_reply = ws.recv(timeout=timeout)
|
raw_reply = ws.recv(timeout=timeout)
|
||||||
reply = json.loads(raw_reply)
|
reply = json.loads(raw_reply)
|
||||||
@@ -897,13 +900,17 @@ def renew_ca_cert_mtls(
|
|||||||
upstream_ca_pem: Optional[str] = None,
|
upstream_ca_pem: Optional[str] = None,
|
||||||
requested_seats: Optional[int] = None,
|
requested_seats: Optional[int] = None,
|
||||||
seat_increment: int = 0,
|
seat_increment: int = 0,
|
||||||
|
seats_taken: Optional[int] = None,
|
||||||
|
active_seats: Optional[int] = None,
|
||||||
|
hostname: Optional[str] = None,
|
||||||
|
server_fingerprint: Optional[str] = None,
|
||||||
timeout: int = 15,
|
timeout: int = 15,
|
||||||
verify_tls: bool = True
|
verify_tls: bool = True
|
||||||
) -> Tuple[bool, str, Optional[str]]:
|
) -> Tuple[bool, str, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Connects to the master License Authority using mutual TLS (mTLS), authenticating
|
Connects to the master License Authority using mutual TLS (mTLS), authenticating
|
||||||
with the server's current ca.crt and ca.key, and requests an automated renewal.
|
with the server's current ca.crt and ca.key, and requests an automated renewal.
|
||||||
Supports requesting seat quota increases or increments during renewal.
|
Supports requesting seat quota increases or increments during renewal and reporting active seat usage.
|
||||||
If renewed, writes backup of previous ca.crt and saves new ca.crt in cert_dir.
|
If renewed, writes backup of previous ca.crt and saves new ca.crt in cert_dir.
|
||||||
Returns (success, message, new_ca_cert_pem).
|
Returns (success, message, new_ca_cert_pem).
|
||||||
"""
|
"""
|
||||||
@@ -921,10 +928,15 @@ def renew_ca_cert_mtls(
|
|||||||
current_pem = f.read()
|
current_pem = f.read()
|
||||||
info = extract_license_info_from_cert(current_pem)
|
info = extract_license_info_from_cert(current_pem)
|
||||||
|
|
||||||
|
effective_seats_taken = seats_taken if seats_taken is not None else active_seats
|
||||||
payload_dict = {
|
payload_dict = {
|
||||||
"license_id": info.get("license_id"),
|
"license_id": info.get("license_id"),
|
||||||
"fingerprint": info.get("fingerprint"),
|
"fingerprint": info.get("fingerprint"),
|
||||||
"current_cert": current_pem
|
"current_cert": current_pem,
|
||||||
|
"seats_taken": effective_seats_taken,
|
||||||
|
"active_seats": effective_seats_taken,
|
||||||
|
"hostname": hostname or "",
|
||||||
|
"server_fingerprint": server_fingerprint or ""
|
||||||
}
|
}
|
||||||
if requested_seats is not None:
|
if requested_seats is not None:
|
||||||
payload_dict["requested_seats"] = requested_seats
|
payload_dict["requested_seats"] = requested_seats
|
||||||
@@ -945,7 +957,7 @@ def renew_ca_cert_mtls(
|
|||||||
ssl_ctx.check_hostname = False
|
ssl_ctx.check_hostname = False
|
||||||
ssl_ctx.verify_mode = ssl.CERT_NONE
|
ssl_ctx.verify_mode = ssl.CERT_NONE
|
||||||
elif upstream_ca_pem:
|
elif upstream_ca_pem:
|
||||||
ssl_ctx.load_verify_locations(cdata=upstream_ca_pem)
|
ssl_ctx.load_verify_locations(cadata=upstream_ca_pem)
|
||||||
upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt")
|
upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt")
|
||||||
if os.path.exists(upstream_ca_path) and verify_tls and not upstream_ca_pem:
|
if os.path.exists(upstream_ca_path) and verify_tls and not upstream_ca_pem:
|
||||||
ssl_ctx.load_verify_locations(cafile=upstream_ca_path)
|
ssl_ctx.load_verify_locations(cafile=upstream_ca_path)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import warnings
|
||||||
|
|
||||||
|
# Suppress cryptography / pgpy deprecation notices during test runs
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
+86
-158
@@ -6,6 +6,11 @@ import sqlite3
|
|||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
# Ensure src/ is importable
|
# Ensure src/ is importable
|
||||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
SRC_DIR = os.path.join(ROOT_DIR, "src")
|
SRC_DIR = os.path.join(ROOT_DIR, "src")
|
||||||
@@ -15,32 +20,68 @@ if SRC_DIR not in sys.path:
|
|||||||
sys.path.insert(0, SRC_DIR)
|
sys.path.insert(0, SRC_DIR)
|
||||||
|
|
||||||
import server_enrollment as enrollment
|
import server_enrollment as enrollment
|
||||||
import License_Authority as authority
|
|
||||||
import Server
|
import Server
|
||||||
|
|
||||||
|
|
||||||
class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
def _create_mock_signed_license_cert(csr_pem: str, seats: int = 50, license_id: str = "LIC-TEST-12345") -> tuple:
|
||||||
|
"""
|
||||||
|
Helper to generate a mock master CA and sign a sub-CA CSR with LOGAR license OIDs
|
||||||
|
using pure cryptography library (without importing the private License_Authority submodule).
|
||||||
|
"""
|
||||||
|
# 1. Generate Root CA
|
||||||
|
ca_key = rsa.generate_private_key(65537, 2048)
|
||||||
|
ca_name = x509.Name([
|
||||||
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "AT"),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LOGAR Master Authority"),
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, "LOGAR-Master-Root-CA")
|
||||||
|
])
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
ca_cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(ca_name)
|
||||||
|
.issuer_name(ca_name)
|
||||||
|
.public_key(ca_key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(now - timedelta(days=1))
|
||||||
|
.not_valid_after(now + timedelta(days=3650))
|
||||||
|
.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
|
||||||
|
.sign(ca_key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
ca_pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||||
|
|
||||||
|
# 2. Parse CSR and sign Subordinate CA with custom OIDs
|
||||||
|
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
|
||||||
|
sub_cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(csr.subject)
|
||||||
|
.issuer_name(ca_name)
|
||||||
|
.public_key(csr.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(now - timedelta(hours=1))
|
||||||
|
.not_valid_after(now + timedelta(days=365))
|
||||||
|
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
|
||||||
|
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_SEATS, str(seats).encode("utf-8")), critical=False)
|
||||||
|
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_LICENSE_ID, license_id.encode("utf-8")), critical=False)
|
||||||
|
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_ISSUER_URL, b"https://logar.eibl.tech"), critical=False)
|
||||||
|
.sign(ca_key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
sub_pem = sub_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||||
|
return sub_pem, ca_pem
|
||||||
|
|
||||||
|
|
||||||
|
class TestServerLicensingAndWebUI(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp")
|
self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp")
|
||||||
os.makedirs(self.test_dir, exist_ok=True)
|
os.makedirs(self.test_dir, exist_ok=True)
|
||||||
self.master_ca_dir = os.path.join(self.test_dir, "master_ca")
|
|
||||||
self.server_cert_dir = os.path.join(self.test_dir, "server_certs")
|
self.server_cert_dir = os.path.join(self.test_dir, "server_certs")
|
||||||
self.db_path = os.path.join(self.test_dir, "license_auth.db")
|
|
||||||
self.server_db = os.path.join(self.test_dir, "server_state.db")
|
self.server_db = os.path.join(self.test_dir, "server_state.db")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
if os.path.exists(self.test_dir):
|
if os.path.exists(self.test_dir):
|
||||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||||
|
|
||||||
def test_master_root_ca_generation(self):
|
|
||||||
"""Verifies Master Root CA creation and structure."""
|
|
||||||
cert_pem, key_pem = authority.generate_master_root_ca(cert_dir=self.master_ca_dir)
|
|
||||||
self.assertIn("BEGIN CERTIFICATE", cert_pem)
|
|
||||||
self.assertIn("BEGIN RSA PRIVATE KEY", key_pem)
|
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.master_ca_dir, "master_root_ca.crt")))
|
|
||||||
|
|
||||||
def test_ca_csr_and_lic_request_generation(self):
|
def test_ca_csr_and_lic_request_generation(self):
|
||||||
"""Verifies subordinate CA CSR and .lic request file generation."""
|
"""Verifies subordinate CA CSR and .lic request file generation on the server."""
|
||||||
lic_data, lic_json = enrollment.create_license_request_lic(
|
lic_data, lic_json = enrollment.create_license_request_lic(
|
||||||
cert_dir=self.server_cert_dir,
|
cert_dir=self.server_cert_dir,
|
||||||
license_server="https://logar.eibl.tech",
|
license_server="https://logar.eibl.tech",
|
||||||
@@ -55,88 +96,26 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
|||||||
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.csr")))
|
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.csr")))
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "logar_server.lic")))
|
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "logar_server.lic")))
|
||||||
|
|
||||||
def test_authority_issue_sub_ca_with_exact_seats(self):
|
def test_license_info_extraction_and_custom_oids(self):
|
||||||
"""Verifies that License Authority signs CSR, embeds exact seat quota via custom OID, and verifies in DB."""
|
"""Verifies extraction of embedded custom seat OIDs and license attributes from certificates."""
|
||||||
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
||||||
result = authority.issue_license_from_csr(
|
sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=42, license_id="LIC-ABC-4242")
|
||||||
csr_pem=csr_pem,
|
|
||||||
seats=42,
|
|
||||||
days_valid=365,
|
|
||||||
server_fingerprint="FP123456",
|
|
||||||
hostname="hub-node-1",
|
|
||||||
db_path=self.db_path,
|
|
||||||
cert_dir=self.master_ca_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(result["status"], "success")
|
info = enrollment.extract_license_info_from_cert(sub_pem)
|
||||||
self.assertEqual(result["seats"], 42)
|
|
||||||
self.assertIn("BEGIN CERTIFICATE", result["ca_cert"])
|
|
||||||
|
|
||||||
# Decode certificate to verify seat quota extraction
|
|
||||||
info = enrollment.extract_license_info_from_cert(result["ca_cert"])
|
|
||||||
self.assertTrue(info["valid"])
|
self.assertTrue(info["valid"])
|
||||||
self.assertEqual(info["seats"], 42)
|
self.assertEqual(info["seats"], 42)
|
||||||
self.assertEqual(info["license_id"], result["license_id"])
|
self.assertEqual(info["license_id"], "LIC-ABC-4242")
|
||||||
self.assertEqual(info["issuer_url"], "https://logar.eibl.tech")
|
self.assertEqual(info["issuer_url"], "https://logar.eibl.tech")
|
||||||
self.assertFalse(info["is_expired"])
|
self.assertFalse(info["is_expired"])
|
||||||
self.assertTrue(info["is_ca"])
|
self.assertTrue(info["is_ca"])
|
||||||
|
|
||||||
# Check authority database
|
|
||||||
lics = authority.list_licenses(db_path=self.db_path)
|
|
||||||
self.assertEqual(len(lics), 1)
|
|
||||||
self.assertEqual(lics[0]["license_id"], result["license_id"])
|
|
||||||
self.assertEqual(lics[0]["seats"], 42)
|
|
||||||
self.assertEqual(lics[0]["status"], "active")
|
|
||||||
|
|
||||||
def test_authority_renewal_and_revocation(self):
|
|
||||||
"""Verifies that an active license cert can be renewed, and revoked licenses are rejected."""
|
|
||||||
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
|
||||||
initial = authority.issue_license_from_csr(
|
|
||||||
csr_pem=csr_pem,
|
|
||||||
seats=15,
|
|
||||||
db_path=self.db_path,
|
|
||||||
cert_dir=self.master_ca_dir
|
|
||||||
)
|
|
||||||
lic_id = initial["license_id"]
|
|
||||||
|
|
||||||
# Successful renewal
|
|
||||||
renewed = authority.renew_license_cert(
|
|
||||||
current_cert_pem=initial["ca_cert"],
|
|
||||||
days_valid=365,
|
|
||||||
db_path=self.db_path,
|
|
||||||
cert_dir=self.master_ca_dir
|
|
||||||
)
|
|
||||||
self.assertEqual(renewed["status"], "renewed")
|
|
||||||
self.assertEqual(renewed["seats"], 15)
|
|
||||||
|
|
||||||
# Check renewal count in DB
|
|
||||||
lics = authority.list_licenses(db_path=self.db_path)
|
|
||||||
self.assertEqual(lics[0]["renewal_count"], 1)
|
|
||||||
|
|
||||||
# Revoke the license
|
|
||||||
revoked = authority.revoke_license(lic_id, db_path=self.db_path)
|
|
||||||
self.assertTrue(revoked)
|
|
||||||
|
|
||||||
# Renewal attempt after revocation must fail
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
authority.renew_license_cert(
|
|
||||||
current_cert_pem=renewed["ca_cert"],
|
|
||||||
db_path=self.db_path,
|
|
||||||
cert_dir=self.master_ca_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_server_license_import_and_seat_update(self):
|
def test_server_license_import_and_seat_update(self):
|
||||||
"""Verifies that importing a signed CA cert updates the server's seat quota in DB and memory."""
|
"""Verifies that importing a signed CA cert updates the server's seat quota in DB and memory."""
|
||||||
# 1. Generate CSR on server side
|
# 1. Generate CSR on server side
|
||||||
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
||||||
|
|
||||||
# 2. Authority signs it with 50 seats
|
# 2. Mock sign it with 50 seats
|
||||||
signed = authority.issue_license_from_csr(
|
sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=50, license_id="LIC-IMPORT-50")
|
||||||
csr_pem=csr_pem,
|
|
||||||
seats=50,
|
|
||||||
db_path=self.db_path,
|
|
||||||
cert_dir=self.master_ca_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Setup mock server state and DB
|
# 3. Setup mock server state and DB
|
||||||
Server.init_db(self.server_db, max_seats=5)
|
Server.init_db(self.server_db, max_seats=5)
|
||||||
@@ -160,16 +139,16 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
|||||||
# 5. Save the signed CA certificate to cert_dir/ca.crt
|
# 5. Save the signed CA certificate to cert_dir/ca.crt
|
||||||
ca_path = os.path.join(self.server_cert_dir, "ca.crt")
|
ca_path = os.path.join(self.server_cert_dir, "ca.crt")
|
||||||
with open(ca_path, "w", encoding="utf-8") as f:
|
with open(ca_path, "w", encoding="utf-8") as f:
|
||||||
f.write(signed["ca_cert"])
|
f.write(sub_pem)
|
||||||
|
|
||||||
# 6. Extract summary after import
|
# 6. Extract summary after import
|
||||||
summary_after = Server.get_license_summary()
|
summary_after = Server.get_license_summary()
|
||||||
self.assertEqual(summary_after["max_seats"], 50)
|
self.assertEqual(summary_after["max_seats"], 50)
|
||||||
self.assertEqual(summary_after["license_id"], signed["license_id"])
|
self.assertEqual(summary_after["license_id"], "LIC-IMPORT-50")
|
||||||
self.assertEqual(summary_after["status"], "ACTIVE")
|
self.assertEqual(summary_after["status"], "ACTIVE")
|
||||||
|
|
||||||
def test_server_client_revocation_and_reactivation(self):
|
def test_server_client_revocation_and_reactivation(self):
|
||||||
"""Verifies revocation and reactivation of edge client seats."""
|
"""Verifies revocation and reactivation of edge client seats on the server hub."""
|
||||||
Server.init_db(self.server_db, max_seats=2)
|
Server.init_db(self.server_db, max_seats=2)
|
||||||
Server.SERVER_STATE["config"] = {
|
Server.SERVER_STATE["config"] = {
|
||||||
"db_path": self.server_db,
|
"db_path": self.server_db,
|
||||||
@@ -202,81 +181,29 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
|||||||
self.assertEqual(summary2["active_seats"], 2)
|
self.assertEqual(summary2["active_seats"], 2)
|
||||||
self.assertEqual(summary2["available_seats"], 0)
|
self.assertEqual(summary2["available_seats"], 0)
|
||||||
|
|
||||||
def test_activated_lic_package_import_and_renewal_seat_increase(self):
|
def test_activated_lic_package_handling(self):
|
||||||
"""Verifies that an activated .lic package can be imported, saving upstream_ca, and renewal expands seats."""
|
"""Verifies that an activated .lic package structure can be processed and verified."""
|
||||||
# 1. Generate CSR on server side
|
|
||||||
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
||||||
|
sub_pem, root_pem = _create_mock_signed_license_cert(csr_pem, seats=20, license_id="LIC-ACT-20")
|
||||||
|
|
||||||
# 2. Authority signs it with 20 seats and produces activated .lic
|
lic_package = {
|
||||||
issued = authority.issue_license_from_csr(
|
"format": "LOGAR_ACTIVATED_LICENSE",
|
||||||
csr_pem=csr_pem,
|
"version": "2.0.1",
|
||||||
seats=20,
|
"license_id": "LIC-ACT-20",
|
||||||
server_fingerprint="FP_TEST_HUB",
|
"seats": 20,
|
||||||
hostname="hub.enterprise.local",
|
|
||||||
db_path=self.db_path,
|
|
||||||
cert_dir=self.master_ca_dir
|
|
||||||
)
|
|
||||||
self.assertIn("lic_package", issued)
|
|
||||||
self.assertIn("lic_json", issued)
|
|
||||||
|
|
||||||
# Write .lic file
|
|
||||||
lic_file = os.path.join(self.test_dir, "test_activated.lic")
|
|
||||||
with open(lic_file, "w", encoding="utf-8") as f:
|
|
||||||
f.write(issued["lic_json"])
|
|
||||||
|
|
||||||
# 3. Server imports .lic package
|
|
||||||
import_res = enrollment.import_license_package(lic_file, cert_dir=self.server_cert_dir)
|
|
||||||
self.assertEqual(import_res["seats"], 20)
|
|
||||||
self.assertEqual(import_res["license_id"], issued["license_id"])
|
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.crt")))
|
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "upstream_ca.crt")))
|
|
||||||
|
|
||||||
# 4. Initialize server state and verify summary
|
|
||||||
Server.init_db(self.server_db, max_seats=20)
|
|
||||||
Server.SERVER_STATE["config"] = {
|
|
||||||
"db_path": self.server_db,
|
|
||||||
"cert_dir": self.server_cert_dir,
|
|
||||||
"max_seats": 20,
|
|
||||||
"tcp_host": "127.0.0.1",
|
|
||||||
"tcp_port": 9443,
|
|
||||||
"hermes_host": "127.0.0.1",
|
|
||||||
"hermes_port": 8443,
|
|
||||||
"server_fingerprint": "FP_TEST_HUB",
|
"server_fingerprint": "FP_TEST_HUB",
|
||||||
"server_name": "Test-Hub",
|
"ca_cert": sub_pem,
|
||||||
"license_server_url": "https://logar.eibl.tech"
|
"master_root_ca": root_pem,
|
||||||
|
"days_valid": 365
|
||||||
}
|
}
|
||||||
summary = Server.get_license_summary()
|
self.assertEqual(lic_package["seats"], 20)
|
||||||
self.assertEqual(summary["max_seats"], 20)
|
self.assertIn("BEGIN CERTIFICATE", lic_package["ca_cert"])
|
||||||
|
self.assertIn("BEGIN CERTIFICATE", lic_package["master_root_ca"])
|
||||||
|
|
||||||
# 5. Authority increments seats by +15 on renewal (total 35)
|
# Verify certificate parses
|
||||||
with open(os.path.join(self.server_cert_dir, "ca.crt"), "r", encoding="utf-8") as f:
|
info = enrollment.extract_license_info_from_cert(lic_package["ca_cert"])
|
||||||
current_ca_pem = f.read()
|
self.assertEqual(info["seats"], 20)
|
||||||
|
self.assertEqual(info["license_id"], "LIC-ACT-20")
|
||||||
renewed = authority.renew_license_cert(
|
|
||||||
current_cert_pem=current_ca_pem,
|
|
||||||
seat_increment=15,
|
|
||||||
db_path=self.db_path,
|
|
||||||
cert_dir=self.master_ca_dir
|
|
||||||
)
|
|
||||||
self.assertEqual(renewed["seats"], 35)
|
|
||||||
|
|
||||||
# 6. Server processes the renewed certificate and saves it to ca.crt
|
|
||||||
ca_path = os.path.join(self.server_cert_dir, "ca.crt")
|
|
||||||
with open(ca_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(renewed["ca_cert"])
|
|
||||||
|
|
||||||
new_info = enrollment.extract_license_info_from_cert(renewed["ca_cert"])
|
|
||||||
self.assertEqual(new_info["seats"], 35)
|
|
||||||
|
|
||||||
# Server updates DB and config
|
|
||||||
Server.SERVER_STATE["config"]["max_seats"] = new_info["seats"]
|
|
||||||
conn = sqlite3.connect(self.server_db)
|
|
||||||
conn.execute("UPDATE license_config SET max_seats = ? WHERE id = 1", (new_info["seats"],))
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
summary_renewed = Server.get_license_summary()
|
|
||||||
self.assertEqual(summary_renewed["max_seats"], 35)
|
|
||||||
|
|
||||||
def test_webui_rendering(self):
|
def test_webui_rendering(self):
|
||||||
"""Verifies that the WebUI HTML contains key components, stats, and download triggers."""
|
"""Verifies that the WebUI HTML contains key components, stats, and download triggers."""
|
||||||
@@ -294,10 +221,11 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
|||||||
|
|
||||||
response = Server.serve_webui()
|
response = Server.serve_webui()
|
||||||
html = response.body.decode("utf-8")
|
html = response.body.decode("utf-8")
|
||||||
self.assertIn("LOGAR Fleet Hub & License Management", html.replace("&", "&"))
|
self.assertIn("LOGAR Fleet Hub", html)
|
||||||
self.assertIn("Export .lic Request", html)
|
self.assertNotIn("LOGAR Fleet Hub & License Management", html)
|
||||||
self.assertIn("Import License (.lic / ca.crt)", html)
|
self.assertNotIn("Export .lic Request", html)
|
||||||
self.assertIn("Sync & Renew via mTLS", html.replace("&", "&"))
|
self.assertNotIn("Import License (.lic / ca.crt)", html)
|
||||||
|
self.assertIn("Sync & Renew via mTLS", html)
|
||||||
self.assertIn("logar.eibl.tech", html)
|
self.assertIn("logar.eibl.tech", html)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import unittest
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
@@ -26,7 +27,12 @@ class TestLinuxClientComponent(unittest.TestCase):
|
|||||||
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
|
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
|
||||||
hashes=[HashAlgorithm.SHA256],
|
hashes=[HashAlgorithm.SHA256],
|
||||||
ciphers=[SymmetricKeyAlgorithm.AES256],
|
ciphers=[SymmetricKeyAlgorithm.AES256],
|
||||||
compression=[CompressionAlgorithm.Uncompressed]
|
compression=[
|
||||||
|
CompressionAlgorithm.ZIP,
|
||||||
|
CompressionAlgorithm.ZLIB,
|
||||||
|
CompressionAlgorithm.BZ2,
|
||||||
|
CompressionAlgorithm.Uncompressed
|
||||||
|
]
|
||||||
)
|
)
|
||||||
self.server_priv = key
|
self.server_priv = key
|
||||||
self.server_pub = key.pubkey
|
self.server_pub = key.pubkey
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ def run_tests():
|
|||||||
print(f"[OK] Client enrolled successfully. Certificates stored in {cert_dir}")
|
print(f"[OK] Client enrolled successfully. Certificates stored in {cert_dir}")
|
||||||
|
|
||||||
# 2c. Verify client shows in /api/clients
|
# 2c. Verify client shows in /api/clients
|
||||||
clients_req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/clients")
|
auth_tok = server_conf.get("auth_token") or client_conf.get("auth_token")
|
||||||
|
client_headers = {"Authorization": f"Bearer {auth_tok}"} if auth_tok else {}
|
||||||
|
clients_req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/clients", headers=client_headers)
|
||||||
with urllib.request.urlopen(clients_req, timeout=5) as resp:
|
with urllib.request.urlopen(clients_req, timeout=5) as resp:
|
||||||
clients_data = json.loads(resp.read().decode("utf-8"))
|
clients_data = json.loads(resp.read().decode("utf-8"))
|
||||||
assert clients_data["active_seats"] >= 1
|
assert clients_data["active_seats"] >= 1
|
||||||
@@ -147,7 +149,7 @@ def run_tests():
|
|||||||
return json.loads(resp_bytes.decode("utf-8"))
|
return json.loads(resp_bytes.decode("utf-8"))
|
||||||
|
|
||||||
print("\n=== [3] Testing Temporal Persistence & Direct Anomaly Verification over mTLS ===")
|
print("\n=== [3] Testing Temporal Persistence & Direct Anomaly Verification over mTLS ===")
|
||||||
test_signature = "TestServiceDegraded"
|
test_signature = f"TestServiceDegraded_{int(time.time())}"
|
||||||
candidate_log = [{
|
candidate_log = [{
|
||||||
"server": client_id,
|
"server": client_id,
|
||||||
"os_type": "linux",
|
"os_type": "linux",
|
||||||
@@ -181,7 +183,7 @@ def run_tests():
|
|||||||
print("[OK] Direct verification confirmed: Warning immediately verified for Hermes with run count tracking over mTLS!")
|
print("[OK] Direct verification confirmed: Warning immediately verified for Hermes with run count tracking over mTLS!")
|
||||||
|
|
||||||
print("\n=== [4] Testing Immediate Pass for Errors over mTLS ===")
|
print("\n=== [4] Testing Immediate Pass for Errors over mTLS ===")
|
||||||
error_signature = "TestServiceCrashImmediate"
|
error_signature = f"TestServiceCrashImmediate_{int(time.time())}"
|
||||||
error_log = [{
|
error_log = [{
|
||||||
"server": client_id,
|
"server": client_id,
|
||||||
"os_type": "linux",
|
"os_type": "linux",
|
||||||
@@ -207,7 +209,9 @@ def run_tests():
|
|||||||
print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!")
|
print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!")
|
||||||
|
|
||||||
print("\n=== [5] Testing Hermes Reporting Endpoint (/api/hermes/report) ===")
|
print("\n=== [5] Testing Hermes Reporting Endpoint (/api/hermes/report) ===")
|
||||||
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report")
|
token = server_conf.get("auth_token") or client_conf.get("auth_token")
|
||||||
|
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
||||||
|
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report", headers=headers)
|
||||||
with urllib.request.urlopen(req, timeout=5) as response:
|
with urllib.request.urlopen(req, timeout=5) as response:
|
||||||
assert response.status == 200, f"Expected 200, got {response.status}"
|
assert response.status == 200, f"Expected 200, got {response.status}"
|
||||||
hermes_data = json.loads(response.read().decode("utf-8"))
|
hermes_data = json.loads(response.read().decode("utf-8"))
|
||||||
|
|||||||
@@ -329,6 +329,119 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
if os.path.exists(test_cert_dir):
|
if os.path.exists(test_cert_dir):
|
||||||
shutil.rmtree(test_cert_dir, ignore_errors=True)
|
shutil.rmtree(test_cert_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_user_auth_and_session(self):
|
||||||
|
"""Tests admin password hashing, verification, session tokens, and password change."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
|
||||||
|
# Verify default admin credentials
|
||||||
|
self.assertTrue(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
|
||||||
|
self.assertFalse(Server.verify_user_credentials("admin", "wrong_password", db_path=self.test_db))
|
||||||
|
self.assertFalse(Server.verify_user_credentials("nonexistent", "password", db_path=self.test_db))
|
||||||
|
|
||||||
|
# Test session token generation & validation
|
||||||
|
token = Server.generate_session_token("admin")
|
||||||
|
self.assertEqual(Server.verify_session_token(token), "admin")
|
||||||
|
self.assertIsNone(Server.verify_session_token("invalid.token.structure"))
|
||||||
|
|
||||||
|
# Test changing password
|
||||||
|
Server.change_user_password("admin", "new_secure_pass123", db_path=self.test_db)
|
||||||
|
self.assertFalse(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
|
||||||
|
self.assertTrue(Server.verify_user_credentials("admin", "new_secure_pass123", db_path=self.test_db))
|
||||||
|
|
||||||
|
def test_hermes_tokens_and_auth(self):
|
||||||
|
"""Tests Hermes Bearer token creation, verification, revocation, and deletion."""
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
|
||||||
|
# Test default Hermes token was created on init
|
||||||
|
tokens = Server.list_hermes_tokens(db_path=self.test_db)
|
||||||
|
self.assertEqual(len(tokens), 1)
|
||||||
|
self.assertEqual(tokens[0]["status"], "active")
|
||||||
|
default_token = tokens[0]["token"]
|
||||||
|
self.assertTrue(Server.verify_hermes_token(default_token, db_path=self.test_db))
|
||||||
|
|
||||||
|
# Test creating new token
|
||||||
|
new_tok = Server.create_hermes_token(name="Hermes Autonomous Agent 01", db_path=self.test_db)
|
||||||
|
self.assertIn("token", new_tok)
|
||||||
|
self.assertEqual(new_tok["name"], "Hermes Autonomous Agent 01")
|
||||||
|
self.assertTrue(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||||
|
|
||||||
|
# Test revoking token
|
||||||
|
self.assertTrue(Server.revoke_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||||
|
self.assertFalse(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||||
|
|
||||||
|
# Test deleting token
|
||||||
|
self.assertTrue(Server.delete_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||||
|
tokens_after = Server.list_hermes_tokens(db_path=self.test_db)
|
||||||
|
self.assertEqual(len(tokens_after), 1)
|
||||||
|
|
||||||
|
def test_client_profile_export(self):
|
||||||
|
"""Tests client profile dictionary generation and export formatting."""
|
||||||
|
cfg = Server.load_or_init_config(self.test_config)
|
||||||
|
profile = Server.generate_client_profile_dict(
|
||||||
|
server_host="logar-hub.corp.internal",
|
||||||
|
server_port=9443,
|
||||||
|
client_id="worker-node-101",
|
||||||
|
config_dict=cfg
|
||||||
|
)
|
||||||
|
self.assertEqual(profile["server_host"], "logar-hub.corp.internal")
|
||||||
|
self.assertEqual(profile["server_port"], 9443)
|
||||||
|
self.assertEqual(profile["client_id"], "worker-node-101")
|
||||||
|
self.assertEqual(profile["server_fingerprint"], cfg["server_fingerprint"])
|
||||||
|
self.assertEqual(profile["server_public_key"], cfg["public_key"])
|
||||||
|
self.assertEqual(profile["enrollment_secret"], cfg["enrollment_secret"])
|
||||||
|
self.assertEqual(profile["format"], "LOGAR_CLIENT_PROFILE")
|
||||||
|
|
||||||
|
def test_client_seat_management_and_relative_time(self):
|
||||||
|
"""Tests client listing, seat approval, revocation, deletion, and relative timestamp helper."""
|
||||||
|
Server.init_db(self.test_db, enrollment_secret="test-sec-123", max_seats=5)
|
||||||
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db}
|
||||||
|
|
||||||
|
# Populate test clients
|
||||||
|
conn = sqlite3.connect(self.test_db)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
five_min_ago = (now - timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
two_hours_ago = (now - timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-win', 'WIN-PROD-01', 'windows', 'FP_W1', 'active', ?)", (five_min_ago,))
|
||||||
|
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-lin', 'LIN-PROD-01', 'linux', 'FP_L1', 'active', ?)", (two_hours_ago,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 1. Test listing
|
||||||
|
client_data = Server.list_clients()
|
||||||
|
self.assertEqual(client_data["active_seats"], 2)
|
||||||
|
self.assertEqual(client_data["max_seats"], 5)
|
||||||
|
self.assertEqual(len(client_data["clients"]), 2)
|
||||||
|
self.assertEqual(client_data["clients"][0]["last_activity_relative"], "5m ago")
|
||||||
|
|
||||||
|
# 2. Test revoking
|
||||||
|
rev_req = Server.ClientActionRequest(client_id="c-win")
|
||||||
|
rev_res = Server.revoke_client(rev_req)
|
||||||
|
self.assertEqual(rev_res["status"], "revoked")
|
||||||
|
client_data = Server.list_clients()
|
||||||
|
self.assertEqual(client_data["active_seats"], 1)
|
||||||
|
|
||||||
|
# 3. Test approving / reactivating
|
||||||
|
app_res = Server.approve_client(rev_req)
|
||||||
|
self.assertEqual(app_res["status"], "active")
|
||||||
|
client_data = Server.list_clients()
|
||||||
|
self.assertEqual(client_data["active_seats"], 2)
|
||||||
|
|
||||||
|
# 4. Test deleting client record
|
||||||
|
del_res = Server.delete_client(rev_req)
|
||||||
|
self.assertEqual(del_res["status"], "deleted")
|
||||||
|
client_data = Server.list_clients()
|
||||||
|
self.assertEqual(len(client_data["clients"]), 1)
|
||||||
|
self.assertEqual(client_data["active_seats"], 1)
|
||||||
|
|
||||||
|
# 5. Test format_relative_time edge cases
|
||||||
|
self.assertEqual(Server.format_relative_time(None), "Never")
|
||||||
|
self.assertEqual(Server.format_relative_time(""), "Never")
|
||||||
|
self.assertEqual(Server.format_relative_time("invalid-date-string"), "invalid-date-str")
|
||||||
|
just_now = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
self.assertIn(Server.format_relative_time(just_now), ("0s ago", "1s ago", "Just now"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import unittest
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
@@ -26,7 +27,12 @@ class TestWinClientComponent(unittest.TestCase):
|
|||||||
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
|
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
|
||||||
hashes=[HashAlgorithm.SHA256],
|
hashes=[HashAlgorithm.SHA256],
|
||||||
ciphers=[SymmetricKeyAlgorithm.AES256],
|
ciphers=[SymmetricKeyAlgorithm.AES256],
|
||||||
compression=[CompressionAlgorithm.Uncompressed]
|
compression=[
|
||||||
|
CompressionAlgorithm.ZIP,
|
||||||
|
CompressionAlgorithm.ZLIB,
|
||||||
|
CompressionAlgorithm.BZ2,
|
||||||
|
CompressionAlgorithm.Uncompressed
|
||||||
|
]
|
||||||
)
|
)
|
||||||
self.server_priv = key
|
self.server_priv = key
|
||||||
self.server_pub = key.pubkey
|
self.server_pub = key.pubkey
|
||||||
|
|||||||
Reference in New Issue
Block a user