Compare commits
110
Commits
52be1f23a6
..
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 | ||
|
|
27de5e9e06 | ||
|
|
dacf01d5b5 | ||
|
|
432a488019 | ||
|
|
384d096de2 | ||
|
|
85bae117b6 | ||
|
|
c8b309d0af | ||
|
|
3b2c3e0fd7 | ||
|
|
b7da54feb2 | ||
|
|
2cd7a990b5 | ||
|
|
2980ad2268 | ||
|
|
95c33c1bf3 | ||
|
|
561ce2e037 | ||
|
|
e89ff9fa27 | ||
|
|
494fef07ba | ||
|
|
4be0107329 | ||
|
|
914db938c3 | ||
|
|
506d3fc234 | ||
|
|
ff5caaf8c7 | ||
|
|
63e8ff5492 | ||
|
|
18e28e2797 | ||
|
|
0e9e3f0f64 | ||
|
|
ea8e98218c | ||
|
|
d9cfb06fad | ||
|
|
76812a1711 | ||
|
|
9584b2c229 | ||
|
|
c9e9a5d267 | ||
|
|
91aee0e591 | ||
|
|
86ae3a11bc | ||
|
|
76d53fbd92 | ||
|
|
39790d371b | ||
|
|
6300481349 | ||
|
|
59029aa3d9 | ||
|
|
431352ddce |
@@ -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:
|
||||
name: Run Component Tests & Pipeline Verification
|
||||
if: "!startsWith(github.ref, 'refs/tags/')"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: logar
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install System Dependencies & Python
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -y
|
||||
apt-get install -y python3 python3-pip python3-venv curl
|
||||
fi
|
||||
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
||||
pip3 install -r compilation/requirements.txt --break-system-packages || pip3 install -r compilation/requirements.txt
|
||||
python3 compilation/ensure_pip.py
|
||||
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
|
||||
python3 -m pip install -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install -r compilation/requirements.txt
|
||||
|
||||
- name: Verify Python Syntax
|
||||
run: |
|
||||
@@ -35,7 +34,7 @@ jobs:
|
||||
|
||||
- name: Run Component Unit Tests
|
||||
run: |
|
||||
python3 -m unittest discover -s tests -v
|
||||
PYTHONWARNINGS="ignore" python3 -m unittest discover -s tests -v
|
||||
|
||||
- name: Run End-to-End Pipeline Integration Test
|
||||
run: |
|
||||
@@ -50,10 +49,10 @@ jobs:
|
||||
SERVER_PID=$!
|
||||
echo "[*] Server launched in background with PID $SERVER_PID"
|
||||
|
||||
# 3. Poll Hermes health / report endpoint until server is listening
|
||||
# 3. Poll /health endpoint until server is listening
|
||||
READY=0
|
||||
for i in $(seq 1 20); do
|
||||
if curl -s http://127.0.0.1:8443/api/hermes/report >/dev/null 2>&1; then
|
||||
if python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8443/health', timeout=1)" >/dev/null 2>&1; then
|
||||
echo "[+] LOGAR Server is ready after ${i}s."
|
||||
READY=1
|
||||
break
|
||||
|
||||
@@ -1,38 +1,72 @@
|
||||
name: Release Binaries & Installers
|
||||
name: Release Binaries & Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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
|
||||
default: 'v2.0.1'
|
||||
default: 'v0.3.1'
|
||||
|
||||
jobs:
|
||||
release-linux:
|
||||
name: Build & Release Linux Binaries
|
||||
runs-on: ubuntu-latest
|
||||
release:
|
||||
name: Build & Release Standalone Binaries (Linux & Windows Cross-Compilation)
|
||||
runs-on: logar
|
||||
steps:
|
||||
- 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: |
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -y
|
||||
apt-get install -y python3 python3-pip python3-venv binutils zip
|
||||
echo "=== SYSTEM DIAGNOSTICS ==="
|
||||
uname -a || true
|
||||
python3 --version || true
|
||||
SUDO=""
|
||||
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
||||
SUDO="sudo"
|
||||
fi
|
||||
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
||||
pip3 install pyinstaller -r compilation/requirements.txt --break-system-packages || pip3 install pyinstaller -r compilation/requirements.txt
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
echo "[*] Installing Python shared libraries, headers, NSIS via apt-get..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
$SUDO apt-get clean || true
|
||||
$SUDO apt-get update --allow-releaseinfo-change -o Acquire::Check-Valid-Until=false -y || $SUDO apt-get update -y || true
|
||||
$SUDO apt-get -f install -y || true
|
||||
PY_VER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "3.9")
|
||||
echo "[*] Detected Python version: ${PY_VER}"
|
||||
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends \
|
||||
nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" \
|
||||
python3-dev libpython3-dev build-essential gcc make curl tar gzip zip unzip || true
|
||||
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends nsis nsis-common || true
|
||||
for pkg in nsis nsis-common "libpython${PY_VER}" "libpython${PY_VER}-dev" "python${PY_VER}-dev" python3-dev libpython3-dev build-essential; do
|
||||
$SUDO apt-get install -y --fix-broken --fix-missing --allow-unauthenticated --no-install-recommends "$pkg" || true
|
||||
done
|
||||
echo "[*] Verifying makensis:"
|
||||
which makensis || echo "makensis not in PATH"
|
||||
echo "[*] Verifying libpython shared library:"
|
||||
find /usr/lib /usr/local/lib -name "libpython*.so*" 2>/dev/null || true
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
echo "[*] Installing dependencies via apk..."
|
||||
$SUDO apk add --no-cache nsis python3-dev build-base gcc make curl tar gzip zip unzip || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
echo "[*] Installing dependencies via dnf..."
|
||||
$SUDO dnf install -y mingw32-nsis nsis python3-devel gcc make curl tar gzip zip unzip || true
|
||||
fi
|
||||
python3 compilation/ensure_pip.py
|
||||
python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || python3 -m pip install --upgrade pip 2>/dev/null || true
|
||||
python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt --break-system-packages 2>/dev/null || python3 -m pip install pyinstaller distlib cryptography -r compilation/requirements.txt
|
||||
|
||||
- name: Compile Standalone Linux Binaries
|
||||
- name: Compile Standalone Binaries (Linux & Windows)
|
||||
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:
|
||||
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||
@@ -40,85 +74,3 @@ jobs:
|
||||
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
|
||||
run: |
|
||||
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
|
||||
test_*.json
|
||||
test_*.db
|
||||
.env
|
||||
*.env
|
||||
runner/.env
|
||||
|
||||
# IDE & OS files
|
||||
.vscode/
|
||||
|
||||
+1
-1
Submodule LOGAR-LICENSING updated: bc1472efeb...7bdb7300ec
@@ -1,410 +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, an automated 4-run rule to filter transient warnings, and immediate pass-through for critical errors before reporting verified anomalies to **Hermes**.
|
||||
[](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
|
||||
1. [Core Philosophy](#core-philosophy)
|
||||
2. [Architecture & Data Flow](#architecture--data-flow)
|
||||
3. [mTLS Security, Dynamic PKI & Licensing](#mtls-security-dynamic-pki--licensing)
|
||||
4. [Cloud-Side Temporal Persistence & 4-Run Rule](#cloud-side-temporal-persistence--4-run-rule)
|
||||
5. [Agentic Hermes & Client Management API](#agentic-hermes--client-management-api)
|
||||
6. [Dynamic Machine & Domain Identification](#dynamic-machine--domain-identification)
|
||||
7. [Automated Service Installers (Linux & Windows)](#automated-service-installers-linux--windows)
|
||||
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)
|
||||
## Key Capabilities
|
||||
|
||||
- **Zero-State Edge Forwarders**: Windows (`Win_Client`) and Linux (`Linux_Client`) agents maintain zero local databases, zero cache files, and minimal memory footprint.
|
||||
- **Mutual TLS 1.3 (`:9443`)**: Enforces bidirectional cryptographic identity verification (`ssl.CERT_REQUIRED`) for all streaming log frames.
|
||||
- **Hierarchical PKI & Seat Licensing**: Subordinate CAs automatically enroll and sync seat quotas with the Master Authority (`logar.eibl.tech`) over WebSockets.
|
||||
- **12-Hour Temporal Persistence**: Evaluates anomalies over a rolling 12-hour temporal window, tracking occurrence velocity and first/last seen metrics.
|
||||
- **Direct Hermes AI Provisioning (`:8443`)**: Exposes verified anomalous telemetry directly to Hermes AI via `/api/hermes/report` without heuristic bottlenecks.
|
||||
- **WebUI Dashboard**: Authenticated, 2-tab management console for live anomaly telemetry, client seat leasing, and Sub-CA lifecycle management.
|
||||
|
||||
---
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
### 1. Edge Thinness & Zero State
|
||||
Site agents running on Windows and Linux act strictly as lightweight forwarders:
|
||||
- **No Local Database**: Clients maintain zero 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 & Severity Routing
|
||||
The central Python hub handles state and verification:
|
||||
- State tracking is managed centrally in SQLite (`logar_state.db`).
|
||||
- Candidate issues are evaluated over a **12-hour temporal evaluation window**.
|
||||
- **Warning Persistence (4-Run Rule)**: `WARNING` issues must persist across **at least 4 consecutive runs / cycles** within the 12-hour window to be confirmed as genuine anomalies, automatically filtering transient infrastructure blips.
|
||||
- **Immediate Error Pass**: Critical errors (`ERROR`, `CRITICAL`, `FATAL`) bypass the 4-run threshold and are promoted immediately to `VERIFIED` on their first occurrence.
|
||||
|
||||
### 3. Agentic Integration with Hermes
|
||||
Instead of engineers manually sifting through logs, **Hermes** ingests pre-filtered anomalies directly from the cloud hub (`GET /api/hermes/report`), treating verified errors and 4-run validated warnings as actionable system artifacts to trigger precise notifications and remediations.
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Data Flow
|
||||
## System Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Edge Nodes [Lightweight Edge Forwarders]
|
||||
W[Win_Client.exe / Win_Client.py<br/>Windows Event Log Ingestion]
|
||||
L[Linux_Client.bin / Linux_Client.py<br/>systemd journalctl -p warning]
|
||||
subgraph Authority [Master License Authority - logar.eibl.tech]
|
||||
M_CA[Master Root CA]
|
||||
AUTH_WS[WebSocket Licensing Hub]
|
||||
end
|
||||
|
||||
subgraph Enrollment [Dynamic PKI & Licensing]
|
||||
ENR[POST /api/client/enroll<br/>License Quota & Secret Validation]
|
||||
CA[Internal Root CA<br/>Signs RSA-2048 Client Cert]
|
||||
subgraph ServerHub [LOGAR Server Hub]
|
||||
SUB_CA[Subordinate CA]
|
||||
SEAT_MGR[Dynamic Seat Manager]
|
||||
MTLS_SRV[mTLS 1.3 Ingestion Server<br/>Port 9443]
|
||||
TEMP_ENG[12h Temporal Persistence Engine]
|
||||
PROV_API[Hermes Provisioning & WebUI API<br/>Port 8443]
|
||||
|
||||
SUB_CA --> SEAT_MGR
|
||||
SEAT_MGR --> MTLS_SRV
|
||||
MTLS_SRV --> TEMP_ENG
|
||||
TEMP_ENG --> PROV_API
|
||||
end
|
||||
|
||||
subgraph Transport [mTLS 1.3 Security Layer]
|
||||
MTLS[Mutual TLS 1.3 Handshake<br/>Port 9443 - Client Cert Required]
|
||||
AUTH[Extract Client CN & Fingerprint<br/>Validate Active License in SQLite]
|
||||
subgraph EdgeNodes [Heterogeneous Edge Forwarders]
|
||||
WIN[Win_Client.exe<br/>Windows Event Logs]
|
||||
LIN[Linux_Client.bin<br/>systemd journalctl]
|
||||
end
|
||||
|
||||
subgraph Hub [LOGAR Server Hub]
|
||||
INGEST[Length-Prefixed Frame Ingestion]
|
||||
DB[(SQLite Persistence<br/>active_issues, clients, license_config)]
|
||||
RULE{12h Window &<br/>4-Run Rule}
|
||||
subgraph Downstream [Hermes AI Layer]
|
||||
HERMES[Hermes AI Engine]
|
||||
end
|
||||
|
||||
subgraph Downstream [Hermes Agent & Monitoring]
|
||||
API[FastAPI Reporting & Management<br/>Port 8443]
|
||||
HERMES[Hermes Agent<br/>GET /api/hermes/report]
|
||||
end
|
||||
|
||||
W -->|Auto-Enrollment| ENR
|
||||
L -->|Auto-Enrollment| ENR
|
||||
ENR --> CA
|
||||
CA -->|ca.crt, client.crt, client.key| W
|
||||
CA -->|ca.crt, client.crt, client.key| L
|
||||
|
||||
W -->|mTLS Stream| MTLS
|
||||
L -->|mTLS Stream| MTLS
|
||||
MTLS --> AUTH
|
||||
AUTH --> INGEST
|
||||
INGEST --> RULE
|
||||
RULE --> DB
|
||||
DB --> API
|
||||
API --> HERMES
|
||||
M_CA -->|Signs Sub-CA Cert| SUB_CA
|
||||
AUTH_WS <==>|Live WebSocket Sync| SUB_CA
|
||||
|
||||
WIN -->|1. Auto-Enrollment| SUB_CA
|
||||
LIN -->|1. Auto-Enrollment| SUB_CA
|
||||
WIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
|
||||
LIN ==>|2. mTLS 1.3 Stream :9443| MTLS_SRV
|
||||
PROV_API ==>|3. GET /api/hermes/report| HERMES
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## mTLS Security, Dynamic PKI & Fleet Licensing
|
||||
## Quick Start & Automated Installers
|
||||
|
||||
### 1. TLS 1.3 Mutual Authentication (mTLS)
|
||||
- **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.
|
||||
LOGAR provides automated production service installers for both Linux (`systemd`) and Windows (`services.msc` via NSSM):
|
||||
|
||||
### 2. Master License Authority (`logar.eibl.tech`) & Seat-Bearing Subordinate CA
|
||||
- **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`.
|
||||
### 1. Server Hub Installation
|
||||
|
||||
- **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`).
|
||||
- **Exportable `.lic` Request & Activated `.lic` Packages**:
|
||||
- **Request**: The LOGAR server generates its Subordinate CA private key (`certs/ca.key`), Certificate Signing Request (`certs/ca.csr`), and packages them into an exportable `logar_server.lic` file.
|
||||
- **Activation Package**: The master license authority issues an activated `.lic` package containing the signed Subordinate CA, granted seat quota, server metadata, and the Master Root CA (`upstream_ca.crt`).
|
||||
- Importable via WebUI or CLI (`python src/Server.py --import-lic activated.lic` or `--import-ca`).
|
||||
- **Self-Updating Client Servers & Dynamic Seat Expansion**:
|
||||
- Client servers update themselves against the master authority (`logar.eibl.tech`) over mTLS (`python src/Server.py --sync-license` or via WebUI / automated watchdog).
|
||||
- Seat quotas can be increased with each renewal or updated on-demand on the master authority.
|
||||
- The client server automatically detects the higher seat count in the signed certificate's OID, updates its database and runtime configuration, and reloads its in-flight `ssl.SSLContext` without dropping client connections or restarting services.
|
||||
- **Automated Container Pipeline & Portainer GitOps Deployment**:
|
||||
- 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)) compiling and publishing the container image via daemonless Kaniko to `10.30.0.3:5000/logar-licensing:latest` and `gitea.eibl.tech/me0nline/logar-licensing:latest`.
|
||||
- It deploys seamlessly in Portainer using a GitOps Stack connected directly to [`LOGAR-LICENSING`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git) via [`docker-compose.yml`](file:///u:/max/Desktop/LOGAR/LOGAR-LICENSING/docker-compose.yml) with automatic polling or webhook redeployment and persistent volume storage (`logar_licensing_data`) preserving Root CA keys and database.
|
||||
- Reverse proxy integration (e.g., Nginx Proxy Manager, Caddy) terminates HTTPS on port 443, verifies client mTLS against the downloadable Master Root CA (`/api/ca.crt`), and forwards client certificate headers (`X-SSL-Client-Cert`) to port 8090.
|
||||
|
||||
### 3. Integrated Fleet Licensing & WebUI Dashboard
|
||||
- **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 & 4-Run Rule
|
||||
|
||||
Incoming candidate logs are tracked in SQLite table `active_issues`:
|
||||
- **Issue Fingerprint**: Formatted as `{site_name}:{server}:{signature}`.
|
||||
- **12-Hour Evaluation Window**:
|
||||
- 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`.
|
||||
- **4-Run Rule for Warnings**:
|
||||
- The 4-run persistence threshold applies to `WARNING` (and `INFO`) events to eliminate transient operational noise.
|
||||
- Each distinct run batch increments `run_count`.
|
||||
- Warnings with `run_count < 4` are marked as `TRANSIENT` and excluded from Hermes reports.
|
||||
- When `run_count >= 4` within the active 12-hour window, the warning transitions to `VERIFIED`.
|
||||
- **Immediate Verification for Errors**:
|
||||
- High-severity events (`ERROR`, `CRITICAL`, `FATAL`) **always pass immediately**.
|
||||
- On their very first ingestion (`run_count = 1`), errors are promoted directly to `VERIFIED` and surfaced to Hermes without waiting for 4 runs.
|
||||
|
||||
---
|
||||
|
||||
## Agentic Hermes & 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** (immediate critical errors and warnings verified after 4 consecutive runs within the 12-hour window):
|
||||
```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%) across 4 runs",
|
||||
"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 candidate issues (`TRANSIENT` and `VERIFIED`).
|
||||
|
||||
### `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`.
|
||||
- **Linux (Automated systemd Installer)**:
|
||||
```bash
|
||||
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
|
||||
- Built using **Inno Setup 6** and bundled with **NSSM** (`compilation/nssm.exe`):
|
||||
- **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.
|
||||
- **Windows (Inno Setup / NSIS Installer)**:
|
||||
Download and execute `LOGAR-Server-Setup.exe` from [Releases](https://gitea.eibl.tech/me0nline/LOGAR/releases). Installs `Server.exe` into `C:\Program Files\LOGAR-Server` and registers `LOGAR_Server` Windows service.
|
||||
|
||||
#### Default WebUI Management Credentials
|
||||
Once installed or executed, access the web console:
|
||||
- **URL**: `http://<server-ip>:8443/webui`
|
||||
- **Default Username**: `admin`
|
||||
- **Default Password**: `admin_logar_2026!` *(Configurable via `LOGAR_ADMIN_USER` / `LOGAR_ADMIN_PASS` or in the WebUI user settings)*
|
||||
|
||||
### 2. Edge Forwarder Installation
|
||||
|
||||
- **Linux Client (1-Line Non-Interactive Installer)**:
|
||||
```bash
|
||||
sudo ./compilation/install_linux_client.sh "http://<HUB_HOST>:8443" "<ENROLLMENT_SECRET>"
|
||||
```
|
||||
*Auto-enrolls mTLS certificates to `/etc/logar/certs` (`chmod 700`), generates configuration, and activates `logar-client.service`.*
|
||||
|
||||
- **Windows Client (Inno Setup Installer)**:
|
||||
1. Download and run `LOGAR-Client-Setup.exe` (supports silent install `/VERYSILENT`).
|
||||
2. Place `client_config.json` (exported from Server WebUI) into `C:\Program Files\LOGAR\client_config.json`.
|
||||
3. The `LOGAR_Client` service starts and auto-enrolls immediately.
|
||||
|
||||
*(Alternatively, standalone binaries `./Server.bin` / `./Linux_Client.bin` / `.\Server.exe` / `.\Win_Client.exe` can be executed directly without service installation).*
|
||||
|
||||
---
|
||||
|
||||
## Repository & Shippables Structure
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
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 & 4-run 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
|
||||
├── .gitea/workflows/ # CI/CD pipelines (ci.yml, release.yml)
|
||||
├── compilation/ # Build scripts, Linux installers & Inno Setup scripts
|
||||
│ ├── install_linux_client.sh # Linux client systemd auto-installer
|
||||
│ ├── install_linux_server.sh # Linux server systemd auto-installer
|
||||
│ ├── package_dist.py # Standalone binary cross-compiler
|
||||
│ └── upload_release.py # Gitea release asset publisher
|
||||
├── src/ # Application source code
|
||||
│ ├── Server.py # Central mTLS hub, temporal engine & REST API
|
||||
│ ├── server_enrollment.py # Sub-CA PKI and certificate generator
|
||||
│ ├── Win_Client.py # Windows edge forwarder
|
||||
│ └── Linux_Client.py # Linux edge forwarder
|
||||
├── tests/ # Automated test suites (35 unit tests + pipeline test)
|
||||
├── LOGAR-LICENSING/ # Standalone Master License Authority (Git submodule)
|
||||
├── RELEASE_NOTES.md # Version history and security changelog
|
||||
└── README.md # Repository overview
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Started & Deployment
|
||||
## Ports & REST API Summary
|
||||
|
||||
### 1. Central Server Hub
|
||||
| Port | Protocol | Service | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`9443`** | `TCP / TLS 1.3` | **mTLS Log Ingestion** | Mutual TLS socket listener for edge forwarders |
|
||||
| **`8443`** | `HTTP / REST` | **Management & Hermes API** | WebUI, forwarder enrollment, seat management, and AI reports |
|
||||
|
||||
1. **Install dependencies**:
|
||||
```bash
|
||||
pip install -r compilation/requirements.txt
|
||||
```
|
||||
2. **Start the server** (generates `server_config.json`, 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
|
||||
```
|
||||
### Core Endpoints
|
||||
|
||||
### 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
|
||||
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`.
|
||||
- **`POST /api/client/enroll`**: Forwarder auto-enrollment and proactive certificate renewal.
|
||||
- **`GET /api/clients`**: Returns active seat quota and registered edge forwarder telemetry *(Admin Auth)*.
|
||||
- **`GET /api/hermes/report`**: Structured JSON anomaly stream for Hermes AI reasoning.
|
||||
- **`POST /api/license/sync`**: Triggers immediate WebSocket sync with `logar.eibl.tech` *(Admin Auth)*.
|
||||
- **`GET /health`**: Server health, encryption fingerprint, and mTLS status.
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
## Testing & Cross-Compilation
|
||||
|
||||
### 1. Component Unit Tests
|
||||
```bash
|
||||
# Run full unit test suite (35 tests)
|
||||
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 4-run rule progression.
|
||||
|
||||
### 2. End-to-End 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
|
||||
# Run end-to-end mTLS pipeline integration test
|
||||
python tests/test_pipeline.py
|
||||
|
||||
# Cross-compile native Linux and Windows binaries on Linux runner
|
||||
python compilation/package_dist.py --target all
|
||||
```
|
||||
Tests client enrollment, secret rejection, mTLS TLS 1.3 socket handshake, warning 4-run rule 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)
|
||||
Pushing a release tag automatically triggers the build workflows:
|
||||
```bash
|
||||
git tag v2.0.1
|
||||
git push origin v2.0.1
|
||||
```
|
||||
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
|
||||
```
|
||||
* [Architecture & Design Details](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Architecture-and-Design)
|
||||
* [System State Machines & Subsystem Lifecycles](https://gitea.eibl.tech/me0nline/LOGAR/wiki/State-Machines-and-Lifecycles)
|
||||
* [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
|
||||
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
|
||||
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
|
||||
|
||||
+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.
|
||||
|
||||
### Key Highlights & Changes in v2.0.1:
|
||||
|
||||
- **Consolidated Single Release Automation Workflow (`.gitea/workflows/release.yml`)**:
|
||||
- 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`).
|
||||
- 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.
|
||||
### 🛡️ 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.
|
||||
- **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`).
|
||||
- **Cryptographic Random Secrets**: Server Hub and License Authority session secrets are now generated with 256-bit CSPRNG (`secrets.token_hex(32)`) on initialization.
|
||||
- **License Renewal Validation**: Hardened `/api/license/renew` with explicit PEM structure and database validation, preventing unhandled exceptions.
|
||||
|
||||
### ⚡ 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..."
|
||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}/certs"
|
||||
chmod 700 "${CONFIG_DIR}/certs"
|
||||
|
||||
if [ -f "dist/Linux_Client.bin" ]; then
|
||||
cp dist/Linux_Client.bin "${INSTALL_DIR}/Linux_Client"
|
||||
|
||||
@@ -4,7 +4,31 @@ set -euo pipefail
|
||||
INSTALL_DIR="/opt/logar-server"
|
||||
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"
|
||||
|
||||
if [ -f "dist/Server.bin" ]; then
|
||||
|
||||
@@ -1,27 +1,97 @@
|
||||
[Setup]
|
||||
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
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=LOGAR-Client-Setup
|
||||
PrivilegesRequired=admin
|
||||
Compression=lzma
|
||||
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]
|
||||
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Dirs]
|
||||
Name: "{commonappdata}\LOGAR"; Permissions: users-modify
|
||||
Name: "{code:GetDataDir}"; Permissions: users-modify
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Client ""{app}\Win_Client.exe"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppDirectory ""{app}"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStdout ""{commonappdata}\LOGAR\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 AppStdout ""{code:GetDataDir}\client.log"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppStderr ""{code:GetDataDir}\client_err.log"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Client AppParameters ""--config """"{code:GetDataDir}\client_config.json"""""""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Client"; Flags: runhidden
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Client"; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Client confirm"; Flags: runhidden
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{app}\certs"
|
||||
Type: files; Name: "{app}\client_config.json"
|
||||
Type: files; Name: "{app}\client_state.json"
|
||||
Type: files; Name: "{app}\*.log"
|
||||
Type: filesandordirs; Name: "{app}\runtime"
|
||||
Type: filesandordirs; Name: "{app}"
|
||||
Type: filesandordirs; Name: "{commonappdata}\LOGAR"
|
||||
|
||||
[Code]
|
||||
var
|
||||
DataDirPage: TInputDirWizardPage;
|
||||
|
||||
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]
|
||||
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
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=LOGAR-Server-Setup
|
||||
PrivilegesRequired=admin
|
||||
Compression=lzma
|
||||
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]
|
||||
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Dirs]
|
||||
Name: "{commonappdata}\LOGAR-Server"; Permissions: users-modify
|
||||
Name: "{code:GetDataDir}"; Permissions: users-modify
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\nssm.exe"; Parameters: "install LOGAR_Server ""{app}\Server.exe"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppDirectory ""{app}"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStdout ""{commonappdata}\LOGAR-Server\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 AppStdout ""{code:GetDataDir}\server.log"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppStderr ""{code:GetDataDir}\server_err.log"""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "set LOGAR_Server AppParameters ""--config """"{code:GetDataDir}\server_config.json"""""""; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "start LOGAR_Server"; Flags: runhidden
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\nssm.exe"; Parameters: "stop LOGAR_Server"; Flags: runhidden
|
||||
Filename: "{app}\nssm.exe"; Parameters: "remove LOGAR_Server confirm"; Flags: runhidden
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{app}\certs"
|
||||
Type: files; Name: "{app}\server_config.json"
|
||||
Type: files; Name: "{app}\logar_state.db"
|
||||
Type: files; Name: "{app}\logar_state.db-*"
|
||||
Type: files; Name: "{app}\*.log"
|
||||
Type: filesandordirs; Name: "{app}\runtime"
|
||||
Type: filesandordirs; Name: "{app}"
|
||||
Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server"
|
||||
|
||||
[Code]
|
||||
var
|
||||
DataDirPage: TInputDirWizardPage;
|
||||
|
||||
procedure InitializeWizard;
|
||||
begin
|
||||
DataDirPage := CreateInputDirPage(
|
||||
wpSelectDir,
|
||||
'Data and Configuration Location',
|
||||
'Where should LOGAR Server store its database, certificates, and configuration?',
|
||||
'Select the folder where LOGAR Server should store the database (logar_state.db), configuration (server_config.json), and mTLS certificates.'#13#10#13#10 +
|
||||
'By default, this is stored in Common AppData (C:\ProgramData\LOGAR-Server), which is accessible to background Windows services.',
|
||||
False,
|
||||
''
|
||||
);
|
||||
DataDirPage.Add('Data Directory:');
|
||||
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||
end;
|
||||
|
||||
function GetDataDir(Param: String): String;
|
||||
begin
|
||||
Result := DataDirPage.Values[0];
|
||||
if Result = '' then
|
||||
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
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
|
||||
+386
-29
@@ -12,6 +12,7 @@ SRC_DIR = os.path.join(ROOT_DIR, "src")
|
||||
DIST_DIR = os.path.join(ROOT_DIR, "dist")
|
||||
BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
|
||||
|
||||
|
||||
def clean_and_prep():
|
||||
if os.path.exists(DIST_DIR):
|
||||
shutil.rmtree(DIST_DIR)
|
||||
@@ -20,6 +21,22 @@ def clean_and_prep():
|
||||
shutil.rmtree(BUILD_TEMP)
|
||||
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):
|
||||
print(f"[*] Compiling {binary_name} with PyInstaller...")
|
||||
cmd = [
|
||||
@@ -38,25 +55,15 @@ def build_pyinstaller_binary(script_path, binary_name):
|
||||
raise RuntimeError(f"Failed to build {binary_name}")
|
||||
print(f"[+] Successfully compiled {binary_name}")
|
||||
|
||||
def calculate_sha256(filepath):
|
||||
h = hashlib.sha256()
|
||||
with open(filepath, "rb") as f:
|
||||
while chunk := f.read(65536):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
def write_checksum_file(filename, digest, binary_filename):
|
||||
out_path = os.path.join(DIST_DIR, filename)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"{digest} {binary_filename}\n")
|
||||
print(f"[+] Generated checksum file: {filename} ({digest[:16]}...)")
|
||||
|
||||
def build_linux_zipapp_fallback():
|
||||
print("[*] Packaging Linux standalone zipapp fallback binaries...")
|
||||
# Linux Client zipapp
|
||||
app_dir = os.path.join(BUILD_TEMP, "linux_app")
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
shutil.copy(os.path.join(SRC_DIR, "Linux_Client.py"), os.path.join(app_dir, "Linux_Client.py"))
|
||||
for f in os.listdir(SRC_DIR):
|
||||
if f.endswith(".py"):
|
||||
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_dir, f))
|
||||
client_out = os.path.join(DIST_DIR, "Linux_Client.bin")
|
||||
zipapp.create_archive(
|
||||
source=app_dir,
|
||||
@@ -67,7 +74,9 @@ def build_linux_zipapp_fallback():
|
||||
# Server zipapp
|
||||
srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
|
||||
os.makedirs(srv_dir, exist_ok=True)
|
||||
shutil.copy(os.path.join(SRC_DIR, "Server.py"), os.path.join(srv_dir, "Server.py"))
|
||||
for f in os.listdir(SRC_DIR):
|
||||
if f.endswith(".py"):
|
||||
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(srv_dir, f))
|
||||
server_out = os.path.join(DIST_DIR, "Server.bin")
|
||||
zipapp.create_archive(
|
||||
source=srv_dir,
|
||||
@@ -76,16 +85,297 @@ def build_linux_zipapp_fallback():
|
||||
main="Server:main"
|
||||
)
|
||||
|
||||
def build_windows():
|
||||
print("[*] Compiling Windows standalone executables...")
|
||||
win_client_script = os.path.join(SRC_DIR, "Win_Client.py")
|
||||
build_pyinstaller_binary(win_client_script, "Win_Client")
|
||||
|
||||
server_script = os.path.join(SRC_DIR, "Server.py")
|
||||
build_pyinstaller_binary(server_script, "Server")
|
||||
def prepare_windows_runtime():
|
||||
"""
|
||||
Downloads the official Python 3.11 embeddable 64-bit runtime, configures ._pth,
|
||||
and downloads/unpacks all required dependencies (wheels) for standalone execution.
|
||||
"""
|
||||
runtime_dir = os.path.join(DIST_DIR, "runtime")
|
||||
if os.path.exists(runtime_dir):
|
||||
return runtime_dir
|
||||
|
||||
os.makedirs(runtime_dir, exist_ok=True)
|
||||
site_packages = os.path.join(runtime_dir, "site-packages")
|
||||
os.makedirs(site_packages, exist_ok=True)
|
||||
|
||||
# 1. Download & Extract Python 3.11 embeddable zip
|
||||
embed_zip_name = "python-3.11.9-embed-amd64.zip"
|
||||
embed_zip_path = os.path.join(BUILD_TEMP, embed_zip_name)
|
||||
embed_url = f"https://www.python.org/ftp/python/3.11.9/{embed_zip_name}"
|
||||
|
||||
if not os.path.exists(embed_zip_path):
|
||||
print(f"[*] Downloading Python 3.11 embeddable runtime from {embed_url}...")
|
||||
import urllib.request
|
||||
urllib.request.urlretrieve(embed_url, embed_zip_path)
|
||||
|
||||
import zipfile
|
||||
with zipfile.ZipFile(embed_zip_path, "r") as zf:
|
||||
zf.extractall(runtime_dir)
|
||||
|
||||
# 2. Configure python311._pth
|
||||
pth_file = os.path.join(runtime_dir, "python311._pth")
|
||||
with open(pth_file, "w", encoding="utf-8") as f:
|
||||
f.write("python311.zip\n.\nsite-packages\nsite-packages/win32\nsite-packages/win32/lib\nsite-packages/Pythonwin\nimport site\n")
|
||||
|
||||
# 3. Download and unpack Windows AMD64 dependencies
|
||||
wheels_dir = os.path.join(BUILD_TEMP, "win_wheels")
|
||||
os.makedirs(wheels_dir, exist_ok=True)
|
||||
|
||||
print("[*] Fetching required Windows x64 dependency wheels...")
|
||||
# Build PGPy 0.6.0 wheel from git (supports modern cryptography)
|
||||
try:
|
||||
subprocess.run([
|
||||
sys.executable, "-m", "pip", "wheel", "--no-deps",
|
||||
"git+https://github.com/SecurityInnovation/PGPy.git",
|
||||
"--wheel-dir", wheels_dir
|
||||
], check=False, capture_output=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Download binary wheels for win_amd64 cp311
|
||||
deps = [
|
||||
"cryptography", "fastapi", "uvicorn", "pydantic",
|
||||
"pywin32", "websockets", "pyasn1", "six", "cffi", "pycparser"
|
||||
]
|
||||
pip_cmd = [
|
||||
sys.executable, "-m", "pip", "download",
|
||||
"--only-binary=:all:",
|
||||
"--platform", "win_amd64",
|
||||
"--python-version", "311",
|
||||
"--implementation", "cp",
|
||||
"--abi", "cp311",
|
||||
"--dest", wheels_dir
|
||||
] + deps
|
||||
|
||||
subprocess.run(pip_cmd, check=False, capture_output=True)
|
||||
|
||||
for whl in os.listdir(wheels_dir):
|
||||
if whl.endswith(".whl"):
|
||||
whl_path = os.path.join(wheels_dir, whl)
|
||||
with zipfile.ZipFile(whl_path, "r") as zf:
|
||||
zf.extractall(site_packages)
|
||||
|
||||
# 4. Copy pywin32 system DLLs to runtime root for immediate DLL loader resolution
|
||||
sys32_dir = os.path.join(site_packages, "pywin32_system32")
|
||||
if os.path.exists(sys32_dir):
|
||||
for dll in os.listdir(sys32_dir):
|
||||
if dll.lower().endswith(".dll"):
|
||||
shutil.copy(os.path.join(sys32_dir, dll), runtime_dir)
|
||||
|
||||
print("[+] Successfully assembled standalone Windows Python 3.11 runtime environment.")
|
||||
return runtime_dir
|
||||
|
||||
|
||||
def create_windows_exe_zipapp(script_name, exe_name):
|
||||
"""
|
||||
Cross-compilation helper: Creates a standalone Windows .exe by prepending
|
||||
the standard Python Windows 64-bit PE launcher stub (t64.exe) to a compiled zipapp archive.
|
||||
"""
|
||||
app_temp_dir = os.path.join(BUILD_TEMP, f"win_app_{exe_name}")
|
||||
os.makedirs(app_temp_dir, exist_ok=True)
|
||||
|
||||
for f in os.listdir(SRC_DIR):
|
||||
if f.endswith(".py"):
|
||||
shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_temp_dir, f))
|
||||
|
||||
module_name = os.path.splitext(script_name)[0]
|
||||
zipapp_temp = os.path.join(BUILD_TEMP, f"{exe_name}.zip")
|
||||
zipapp.create_archive(
|
||||
source=app_temp_dir,
|
||||
target=zipapp_temp,
|
||||
interpreter=r"runtime\python.exe",
|
||||
main=f"{module_name}:main"
|
||||
)
|
||||
|
||||
stub_bytes = b""
|
||||
try:
|
||||
import pip._vendor.distlib.resources as distlib_res
|
||||
finder = distlib_res.finder("pip._vendor.distlib")
|
||||
res = finder.find("t64.exe")
|
||||
if res:
|
||||
stub_bytes = res.bytes
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not stub_bytes:
|
||||
try:
|
||||
import distlib.resources as distlib_res
|
||||
finder = distlib_res.finder("distlib")
|
||||
res = finder.find("t64.exe")
|
||||
if res:
|
||||
stub_bytes = res.bytes
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
out_exe_path = os.path.join(DIST_DIR, f"{exe_name}.exe")
|
||||
with open(out_exe_path, "wb") as out_f:
|
||||
if stub_bytes:
|
||||
out_f.write(stub_bytes)
|
||||
with open(zipapp_temp, "rb") as in_f:
|
||||
out_f.write(in_f.read())
|
||||
print(f"[+] Successfully cross-compiled Windows PE executable: {exe_name}.exe (PE Stub attached: {bool(stub_bytes)})")
|
||||
|
||||
|
||||
def ensure_makensis():
|
||||
"""Finds or attempts to install makensis on the current system."""
|
||||
for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis",
|
||||
r"C:\Program Files (x86)\NSIS\makensis.exe",
|
||||
r"C:\Program Files\NSIS\makensis.exe"]:
|
||||
p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd
|
||||
if p and (os.path.exists(p) or shutil.which(p)):
|
||||
return p
|
||||
|
||||
if platform.system() == "Linux":
|
||||
print("[*] Attempting to install 'nsis' package dynamically...")
|
||||
sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else []
|
||||
if shutil.which("apt-get"):
|
||||
try:
|
||||
subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False)
|
||||
subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False)
|
||||
subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", "nsis", "nsis-common"], check=False)
|
||||
except Exception as e:
|
||||
print(f"[!] Warning: Dynamic apt install failed: {e}")
|
||||
elif shutil.which("apk"):
|
||||
try:
|
||||
subprocess.run(sudo + ["apk", "add", "--no-cache", "nsis"], check=False)
|
||||
except Exception:
|
||||
pass
|
||||
elif shutil.which("dnf"):
|
||||
try:
|
||||
subprocess.run(sudo + ["dnf", "install", "-y", "mingw32-nsis", "nsis"], check=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis"]:
|
||||
p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd
|
||||
if p and (os.path.exists(p) or shutil.which(p)):
|
||||
return p
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def ensure_libpython():
|
||||
"""Attempts to install libpython shared library on Debian/Ubuntu for PyInstaller."""
|
||||
if platform.system() == "Linux" and shutil.which("apt-get"):
|
||||
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else []
|
||||
try:
|
||||
subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False)
|
||||
subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False)
|
||||
subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", f"libpython{py_ver}", "python3-dev", f"python{py_ver}-dev", "libpython3-dev"], check=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def compile_nsis_installers():
|
||||
"""Compiles NSIS installers if makensis is available (native Linux/Windows)."""
|
||||
makensis_bin = ensure_makensis()
|
||||
if not makensis_bin:
|
||||
print("[!] Note: NSIS compiler (makensis) not found in PATH or standard directories.")
|
||||
return False
|
||||
|
||||
makensis_cmd = [makensis_bin]
|
||||
compilation_dir = os.path.join(ROOT_DIR, "compilation")
|
||||
success = True
|
||||
for nsi in ["installer_client.nsi", "installer_server.nsi"]:
|
||||
nsi_path = os.path.join(compilation_dir, nsi)
|
||||
if os.path.exists(nsi_path):
|
||||
print(f"[*] Compiling NSIS script: {nsi} via {makensis_cmd[0]}...")
|
||||
try:
|
||||
subprocess.run(makensis_cmd + [nsi_path], cwd=compilation_dir, check=True)
|
||||
print(f"[+] Successfully compiled NSIS installer for {nsi}")
|
||||
except Exception as e:
|
||||
print(f"[!] Warning: Failed to compile {nsi} with {makensis_cmd}: {e}")
|
||||
success = False
|
||||
return success
|
||||
|
||||
|
||||
def compile_inno_setup_installers():
|
||||
"""Compiles Inno Setup installers if iscc or innosetup wrapper is available."""
|
||||
iscc_cmd = None
|
||||
user_iscc = os.path.expanduser(r"~\AppData\Local\Programs\Inno Setup 6\ISCC.exe")
|
||||
if shutil.which("iscc"):
|
||||
iscc_cmd = ["iscc"]
|
||||
elif shutil.which("innosetup"):
|
||||
iscc_cmd = ["innosetup"]
|
||||
elif os.path.exists(user_iscc):
|
||||
iscc_cmd = [user_iscc]
|
||||
elif os.path.exists("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"):
|
||||
iscc_cmd = ["C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"]
|
||||
elif os.path.exists("C:\\Program Files\\Inno Setup 6\\ISCC.exe"):
|
||||
iscc_cmd = ["C:\\Program Files\\Inno Setup 6\\ISCC.exe"]
|
||||
elif shutil.which("wine") and os.path.exists("/opt/innosetup/app/ISCC.exe"):
|
||||
iscc_cmd = ["wine", "/opt/innosetup/app/ISCC.exe"]
|
||||
elif shutil.which("wine"):
|
||||
wine_iscc = os.path.expanduser("~/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe")
|
||||
if os.path.exists(wine_iscc):
|
||||
iscc_cmd = ["wine", wine_iscc]
|
||||
|
||||
if not iscc_cmd:
|
||||
print("[!] Note: Inno Setup compiler (iscc / innosetup) not available on host. Skipping installer .exe creation.")
|
||||
return False
|
||||
|
||||
for iss in ["installer_client.iss", "installer_server.iss"]:
|
||||
iss_path = os.path.join(ROOT_DIR, "compilation", iss)
|
||||
if os.path.exists(iss_path):
|
||||
print(f"[*] Compiling Inno Setup script: {iss}...")
|
||||
try:
|
||||
subprocess.run(iscc_cmd + [iss_path], check=True)
|
||||
print(f"[+] Successfully compiled {iss}")
|
||||
except Exception as e:
|
||||
print(f"[!] Warning: Failed to compile {iss} with {iscc_cmd}: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def compile_windows_installers():
|
||||
if not compile_nsis_installers():
|
||||
compile_inno_setup_installers()
|
||||
|
||||
|
||||
def build_windows():
|
||||
print("[*] Packaging self-contained Windows standalone executables...")
|
||||
prepare_windows_runtime()
|
||||
create_windows_exe_zipapp("Win_Client.py", "Win_Client")
|
||||
create_windows_exe_zipapp("Server.py", "Server")
|
||||
|
||||
compile_windows_installers()
|
||||
|
||||
client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
|
||||
server_bin = os.path.join(DIST_DIR, "Server.exe")
|
||||
runtime_dir = os.path.join(DIST_DIR, "runtime")
|
||||
|
||||
# Windows Zip Bundles
|
||||
import zipfile
|
||||
if os.path.exists(client_bin):
|
||||
client_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Client.zip")
|
||||
with zipfile.ZipFile(client_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.write(client_bin, arcname="Win_Client.exe")
|
||||
sample_cfg = os.path.join(ROOT_DIR, "out", "win_client", "client_config.sample.json")
|
||||
if os.path.exists(sample_cfg):
|
||||
zf.write(sample_cfg, arcname="client_config.sample.json")
|
||||
if os.path.exists(runtime_dir):
|
||||
for root, _, files in os.walk(runtime_dir):
|
||||
for file in files:
|
||||
full_p = os.path.join(root, file)
|
||||
rel_p = os.path.relpath(full_p, DIST_DIR)
|
||||
zf.write(full_p, arcname=rel_p)
|
||||
print(f"[+] Packaged Windows Client Zip: {os.path.basename(client_zip)}")
|
||||
|
||||
if os.path.exists(server_bin):
|
||||
server_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Server.zip")
|
||||
with zipfile.ZipFile(server_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.write(server_bin, arcname="Server.exe")
|
||||
sample_cfg = os.path.join(ROOT_DIR, "server_config.sample.json")
|
||||
if os.path.exists(sample_cfg):
|
||||
zf.write(sample_cfg, arcname="server_config.sample.json")
|
||||
if os.path.exists(runtime_dir):
|
||||
for root, _, files in os.walk(runtime_dir):
|
||||
for file in files:
|
||||
full_p = os.path.join(root, file)
|
||||
rel_p = os.path.relpath(full_p, DIST_DIR)
|
||||
zf.write(full_p, arcname=rel_p)
|
||||
print(f"[+] Packaged Windows Server Zip: {os.path.basename(server_zip)}")
|
||||
|
||||
sums = []
|
||||
if os.path.exists(client_bin):
|
||||
@@ -103,20 +393,33 @@ def build_windows():
|
||||
else:
|
||||
print(f"[!] Warning: Expected {server_bin} was not found.")
|
||||
|
||||
# Inno Setup output checksums if present
|
||||
for inst in ["LOGAR-Client-Setup.exe", "LOGAR-Server-Setup.exe", "LOGAR-Windows-Client.zip", "LOGAR-Windows-Server.zip"]:
|
||||
inst_path = os.path.join(DIST_DIR, inst)
|
||||
if os.path.exists(inst_path):
|
||||
inst_hash = calculate_sha256(inst_path)
|
||||
sums.append(f"{inst_hash} {inst}")
|
||||
|
||||
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_windows.txt")
|
||||
with open(sums_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(sums) + "\n")
|
||||
|
||||
|
||||
def build_linux():
|
||||
print("[*] Compiling Linux standalone binaries...")
|
||||
print("[*] Compiling Linux standalone binaries and packaging installers...")
|
||||
is_linux_host = platform.system() == "Linux"
|
||||
|
||||
if is_linux_host:
|
||||
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py")
|
||||
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
|
||||
ensure_libpython()
|
||||
try:
|
||||
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py")
|
||||
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
|
||||
|
||||
server_script = os.path.join(SRC_DIR, "Server.py")
|
||||
build_pyinstaller_binary(server_script, "Server.bin")
|
||||
server_script = os.path.join(SRC_DIR, "Server.py")
|
||||
build_pyinstaller_binary(server_script, "Server.bin")
|
||||
except Exception as e:
|
||||
print(f"[!] PyInstaller compilation encountered an issue ({e}). Falling back to standalone zipapp packaging...")
|
||||
build_linux_zipapp_fallback()
|
||||
|
||||
# Normalize extensions in case PyInstaller dropped .bin
|
||||
for name in ["Linux_Client", "Server"]:
|
||||
@@ -137,9 +440,37 @@ def build_linux():
|
||||
print("[!] Note: Host platform is not Linux. Generating executable zipapps for Linux target.")
|
||||
build_linux_zipapp_fallback()
|
||||
|
||||
# Copy Linux installation scripts directly to dist
|
||||
for sh in ["install_linux_client.sh", "install_linux_server.sh"]:
|
||||
sh_src = os.path.join(ROOT_DIR, "compilation", sh)
|
||||
if os.path.exists(sh_src):
|
||||
sh_dst = os.path.join(DIST_DIR, sh)
|
||||
shutil.copy(sh_src, sh_dst)
|
||||
print(f"[+] Copied Linux installer script: {sh}")
|
||||
|
||||
# Package Linux tarballs
|
||||
import tarfile
|
||||
client_bin = os.path.join(DIST_DIR, "Linux_Client.bin")
|
||||
server_bin = os.path.join(DIST_DIR, "Server.bin")
|
||||
|
||||
if os.path.exists(client_bin):
|
||||
client_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Client.tar.gz")
|
||||
with tarfile.open(client_tar, "w:gz") as tf:
|
||||
tf.add(client_bin, arcname="Linux_Client.bin")
|
||||
client_sh = os.path.join(DIST_DIR, "install_linux_client.sh")
|
||||
if os.path.exists(client_sh):
|
||||
tf.add(client_sh, arcname="install_linux_client.sh")
|
||||
print(f"[+] Packaged Linux Client Tarball: {os.path.basename(client_tar)}")
|
||||
|
||||
if os.path.exists(server_bin):
|
||||
server_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Server.tar.gz")
|
||||
with tarfile.open(server_tar, "w:gz") as tf:
|
||||
tf.add(server_bin, arcname="Server.bin")
|
||||
server_sh = os.path.join(DIST_DIR, "install_linux_server.sh")
|
||||
if os.path.exists(server_sh):
|
||||
tf.add(server_sh, arcname="install_linux_server.sh")
|
||||
print(f"[+] Packaged Linux Server Tarball: {os.path.basename(server_tar)}")
|
||||
|
||||
sums = []
|
||||
if os.path.exists(client_bin):
|
||||
client_hash = calculate_sha256(client_bin)
|
||||
@@ -156,16 +487,39 @@ def build_linux():
|
||||
else:
|
||||
print(f"[!] Warning: Expected {server_bin} was not found.")
|
||||
|
||||
for item in ["install_linux_client.sh", "install_linux_server.sh", "LOGAR-Linux-Client.tar.gz", "LOGAR-Linux-Server.tar.gz"]:
|
||||
item_path = os.path.join(DIST_DIR, item)
|
||||
if os.path.exists(item_path):
|
||||
item_hash = calculate_sha256(item_path)
|
||||
sums.append(f"{item_hash} {item}")
|
||||
|
||||
sums_path = os.path.join(DIST_DIR, "SHA256SUMS_linux.txt")
|
||||
with open(sums_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(sums) + "\n")
|
||||
|
||||
|
||||
def build_all():
|
||||
print("[*] Building standalone distribution artifacts for both Linux and Windows...")
|
||||
build_linux()
|
||||
build_windows()
|
||||
|
||||
# Consolidated master checksums file
|
||||
all_sums = []
|
||||
for f in sorted(os.listdir(DIST_DIR)):
|
||||
fpath = os.path.join(DIST_DIR, f)
|
||||
if os.path.isfile(fpath) and not f.endswith(".txt") and not "sha256sum" in f:
|
||||
h = calculate_sha256(fpath)
|
||||
all_sums.append(f"{h} {f}")
|
||||
with open(os.path.join(DIST_DIR, "SHA256SUMS.txt"), "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(all_sums) + "\n")
|
||||
|
||||
|
||||
def main(target=None):
|
||||
if target is None:
|
||||
parser = argparse.ArgumentParser(description="LOGAR Standalone Binary Compiler & Packager")
|
||||
parser.add_argument(
|
||||
"--target", "-t",
|
||||
choices=["windows", "win", "linux", "auto"],
|
||||
choices=["all", "windows", "win", "linux", "auto"],
|
||||
default="auto",
|
||||
help="Target platform to compile binaries for (default: auto-detect)"
|
||||
)
|
||||
@@ -173,19 +527,21 @@ def main(target=None):
|
||||
target = args.target
|
||||
|
||||
if target == "auto":
|
||||
target = "windows" if platform.system() == "Windows" else "linux"
|
||||
target = "all"
|
||||
elif target == "win":
|
||||
target = "windows"
|
||||
|
||||
print("=" * 60)
|
||||
print(" LOGAR Binary Packaging")
|
||||
print(" LOGAR Binary Packaging & Cross-Compilation")
|
||||
print(f" Host Platform: {platform.system()} ({platform.machine()})")
|
||||
print(f" Target Platform: {target.upper()}")
|
||||
print("=" * 60)
|
||||
|
||||
clean_and_prep()
|
||||
|
||||
if target == "windows":
|
||||
if target == "all":
|
||||
build_all()
|
||||
elif target == "windows":
|
||||
build_windows()
|
||||
elif target == "linux":
|
||||
build_linux()
|
||||
@@ -202,5 +558,6 @@ def main(target=None):
|
||||
print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -3,5 +3,7 @@ standard-imghdr>=3.13.0; python_version >= "3.13"
|
||||
cryptography>=42.0.0
|
||||
fastapi>=0.110.0
|
||||
uvicorn>=0.28.0
|
||||
websockets>=11.0.0
|
||||
pydantic>=2.6.0
|
||||
distlib>=0.3.8
|
||||
pywin32>=306; sys_platform == "win32"
|
||||
|
||||
@@ -13,9 +13,21 @@ import time
|
||||
DEFAULT_GITEA_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.eibl.tech")
|
||||
DEFAULT_REPO = os.environ.get("GITEA_REPOSITORY", "me0nline/LOGAR")
|
||||
|
||||
import base64
|
||||
|
||||
def get_auth_header(token):
|
||||
if not token:
|
||||
return ""
|
||||
if token.startswith("Basic ") or token.startswith("token ") or token.startswith("Bearer "):
|
||||
return token
|
||||
if ":" in token:
|
||||
b64 = base64.b64encode(token.encode("utf-8")).decode("ascii")
|
||||
return f"Basic {b64}"
|
||||
return f"token {token}"
|
||||
|
||||
def get_existing_assets(base_url, repo, release_id, token):
|
||||
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}", "Accept": "application/json"})
|
||||
req = urllib.request.Request(url, headers={"Authorization": get_auth_header(token), "Accept": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
@@ -24,7 +36,7 @@ def get_existing_assets(base_url, repo, release_id, token):
|
||||
|
||||
def delete_asset(base_url, repo, release_id, asset_id, token):
|
||||
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}"
|
||||
req = urllib.request.Request(url, method="DELETE", headers={"Authorization": f"token {token}"})
|
||||
req = urllib.request.Request(url, method="DELETE", headers={"Authorization": get_auth_header(token)})
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
pass
|
||||
@@ -47,7 +59,7 @@ def upload_file_to_release(base_url, repo, release_id, token, file_path, max_ret
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
req = urllib.request.Request(url, data=file_bytes, method="POST")
|
||||
req.add_header("Authorization", f"token {token}")
|
||||
req.add_header("Authorization", get_auth_header(token))
|
||||
req.add_header("Content-Type", "application/octet-stream")
|
||||
req.add_header("Accept", "application/json")
|
||||
|
||||
@@ -73,14 +85,14 @@ def upload_file_to_release(base_url, repo, release_id, token, file_path, max_ret
|
||||
def create_or_get_release(base_url, repo, tag, token, title=None, notes=None):
|
||||
url = f"{base_url}/api/v1/repos/{repo}/releases"
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Authorization": get_auth_header(token),
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
# Check if release exists
|
||||
check_url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{urllib.parse.quote(tag)}"
|
||||
check_req = urllib.request.Request(check_url, headers={"Authorization": f"token {token}"})
|
||||
check_req = urllib.request.Request(check_url, headers={"Authorization": get_auth_header(token)})
|
||||
try:
|
||||
with urllib.request.urlopen(check_req) as resp:
|
||||
existing = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
@@ -14,7 +14,7 @@ Standalone compiled binary and automated systemd service distribution for Linux
|
||||
- **Master License Authority (`logar.eibl.tech`) & Seat-Bearing CA**: Integrates with the upstream authority. Subordinate CA certificates embed exact seat quotas via custom enterprise X.509 OID `1.3.6.1.4.1.59999.1.1`.
|
||||
- **Zero-Touch mTLS Renewals**: Once initial signed CA cert is imported, the server hub automatically executes in-flight mTLS renewals with `https://logar.eibl.tech/api/license/renew` when nearing expiration (< 30 days).
|
||||
- **Fleet Management & WebUI Dashboard (Port 8443)**: Modern browser WebUI accessible at `http://localhost:8443/webui` to monitor seat quotas, export `.lic` request files, copy CA CSRs, activate signed CA certs, and revoke/reactivate forwarder seats with immediate socket enforcement.
|
||||
- **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events require persistence across at least 4 distinct client transmission cycles within a rolling 12-hour evaluation window.
|
||||
- **Direct Hermes Intelligence & Temporal Telemetry**: All ingested actionable anomalies (`WARNING`, `ERROR`, `INFO`) are immediately promoted to `VERIFIED` and surfaced directly to Hermes with rich temporal metadata (run counts, timestamps) across a rolling 12-hour evaluation window, allowing Hermes AI to perform all contextual reasoning and decisions.
|
||||
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`.
|
||||
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
|
||||
|
||||
@@ -58,7 +58,7 @@ Output:
|
||||
Transport Security: mTLS (TLS 1.3)
|
||||
License Quota: 10 Active Seats
|
||||
Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
|
||||
Evaluation Window: 12 hours | 4-Run Rule: Warnings | Immediate Pass: Errors
|
||||
Evaluation Window: 12 hours | Direct Hermes Reasoning Enabled
|
||||
============================================================
|
||||
[*] LOGAR mTLS TLSv1.3 Socket Server listening on 0.0.0.0:9443
|
||||
[*] Hermes Reporting API available at http://0.0.0.0:8443/api/hermes/report
|
||||
@@ -81,7 +81,7 @@ The generated `server_config.json` contains:
|
||||
"tls_enabled": true,
|
||||
"db_path": "logar_state.db",
|
||||
"evaluation_window_hours": 12,
|
||||
"min_persistence_runs": 4,
|
||||
"min_persistence_runs": 1,
|
||||
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
|
||||
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
||||
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
|
||||
@@ -131,7 +131,7 @@ Synchronize license and check for seat quota increases against the master author
|
||||
```
|
||||
|
||||
### Upstream Authority Deployment (Portainer GitOps & Reverse Proxy)
|
||||
The upstream Master License Authority service (`logar.eibl.tech`) is compiled and deployed via Portainer GitOps from the [`LOGAR-LICENSING`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git) repository, running from `10.30.0.3:5000/logar-licensing:latest` (or `gitea.eibl.tech/me0nline/logar-licensing:latest`) with automated pipeline compilation and persistent volume storage. Public traffic passes through a reverse proxy (e.g., Nginx Proxy Manager, Caddy) which validates mTLS client certificates against the Master Root CA and forwards client credentials.
|
||||
The upstream Master License Authority service (`logar.eibl.tech`) is compiled and deployed via Portainer GitOps from the [`LOGAR-LICENSING`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git) repository, running from `gitea.eibl.tech/me0nline/logar-licensing:latest` with automated pipeline compilation and persistent volume storage. Public traffic passes through a reverse proxy (e.g., Nginx Proxy Manager with **Websockets Support** enabled), handling WebSocket licensing handshakes (`wss://logar.eibl.tech/ws/license`), multi-tenant fleet isolation, user management, and authenticated WebUI dashboard access.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Standalone compiled executable and installer distribution for Windows Server env
|
||||
- **Master License Authority (`logar.eibl.tech`) & Seat-Bearing CA**: Integrates with the upstream authority. Subordinate CA certificates embed exact seat quotas via custom enterprise X.509 OID `1.3.6.1.4.1.59999.1.1`.
|
||||
- **Zero-Touch mTLS Renewals**: Once initial signed CA cert is imported, the server hub automatically executes in-flight mTLS renewals with `https://logar.eibl.tech/api/license/renew` when nearing expiration (< 30 days).
|
||||
- **Fleet Management & WebUI Dashboard (Port 8443)**: Modern browser WebUI accessible at `http://localhost:8443/webui` to monitor seat quotas, export `.lic` request files, copy CA CSRs, activate signed CA certs, and revoke/reactivate forwarder seats with immediate socket enforcement.
|
||||
- **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events require persistence across at least 4 distinct transmission cycles within a rolling 12-hour evaluation window.
|
||||
- **Direct Hermes Intelligence & Temporal Telemetry**: All ingested actionable anomalies (`WARNING`, `ERROR`, `INFO`) are immediately promoted to `VERIFIED` and surfaced directly to Hermes with rich temporal metadata (run counts, timestamps) across a rolling 12-hour evaluation window, allowing Hermes AI to perform all contextual reasoning and decisions.
|
||||
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`.
|
||||
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
|
||||
|
||||
@@ -58,7 +58,7 @@ Output:
|
||||
Transport Security: mTLS (TLS 1.3)
|
||||
License Quota: 10 Active Seats
|
||||
Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
|
||||
Evaluation Window: 12 hours | 4-Run Rule: Warnings | Immediate Pass: Errors
|
||||
Evaluation Window: 12 hours | Direct Hermes Reasoning Enabled
|
||||
============================================================
|
||||
[*] LOGAR mTLS TLSv1.3 Socket Server listening on 0.0.0.0:9443
|
||||
[*] Hermes Reporting API available at http://0.0.0.0:8443/api/hermes/report
|
||||
@@ -81,7 +81,7 @@ The generated `server_config.json` contains:
|
||||
"tls_enabled": true,
|
||||
"db_path": "logar_state.db",
|
||||
"evaluation_window_hours": 12,
|
||||
"min_persistence_runs": 4,
|
||||
"min_persistence_runs": 1,
|
||||
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
|
||||
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
||||
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
|
||||
@@ -131,7 +131,7 @@ Synchronize license and check for seat quota increases against the master author
|
||||
```
|
||||
|
||||
### Upstream Authority Deployment (Portainer GitOps & Reverse Proxy)
|
||||
The upstream Master License Authority service (`logar.eibl.tech`) is compiled and deployed via Portainer GitOps from the [`LOGAR-LICENSING`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git) repository, running from `10.30.0.3:5000/logar-licensing:latest` (or `gitea.eibl.tech/me0nline/logar-licensing:latest`) with automated pipeline compilation and persistent volume storage. Public traffic passes through a reverse proxy (e.g., Nginx Proxy Manager, Caddy) which validates mTLS client certificates against the Master Root CA and forwards client credentials.
|
||||
The upstream Master License Authority service (`logar.eibl.tech`) is compiled and deployed via Portainer GitOps from the [`LOGAR-LICENSING`](https://gitea.eibl.tech/me0nline/LOGAR-LICENSING.git) repository, running from `gitea.eibl.tech/me0nline/logar-licensing:latest` with automated pipeline compilation and persistent volume storage. Public traffic passes through a reverse proxy (e.g., Nginx Proxy Manager with **Websockets Support** enabled), handling WebSocket licensing handshakes (`wss://logar.eibl.tech/ws/license`), multi-tenant fleet isolation, user management, and authenticated WebUI dashboard access.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 sys
|
||||
import importlib.util
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
+3
-1
@@ -9,7 +9,7 @@ import subprocess
|
||||
import urllib.request
|
||||
import warnings
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import Optional
|
||||
|
||||
# Suppress cryptography / pgpy deprecation notices
|
||||
warnings.filterwarnings("ignore")
|
||||
@@ -334,6 +334,8 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
|
||||
hub_url = None
|
||||
if enrollment_secret:
|
||||
hermes_host = config.get("hermes_host", server_host)
|
||||
if hermes_host in ("0.0.0.0", "::", None):
|
||||
hermes_host = server_host if server_host not in ("0.0.0.0", "::", None) else "127.0.0.1"
|
||||
hermes_port = config.get("hermes_port", 8443)
|
||||
hub_url = f"http://{hermes_host}:{hermes_port}"
|
||||
try:
|
||||
|
||||
+3042
-1779
File diff suppressed because it is too large
Load Diff
+87
-4
@@ -8,7 +8,7 @@ import argparse
|
||||
import urllib.request
|
||||
import warnings
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import Optional
|
||||
|
||||
# Suppress cryptography / pgpy deprecation notices
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
if custom_state_path:
|
||||
return custom_state_path
|
||||
config_dir = os.path.dirname(os.path.abspath(config_path))
|
||||
if sys.platform == "win32" and ("Program Files" in config_dir or not is_path_writable(config_dir)):
|
||||
return os.path.join(get_default_data_dir("LOGAR"), STATE_FILE_NAME)
|
||||
return os.path.join(config_dir, STATE_FILE_NAME)
|
||||
|
||||
|
||||
@@ -135,6 +205,7 @@ def load_state(state_path: str) -> dict:
|
||||
|
||||
def save_state(state_path: str, state: dict):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(os.path.abspath(state_path)), exist_ok=True)
|
||||
temp_path = f"{state_path}.tmp"
|
||||
with open(temp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
@@ -154,13 +225,23 @@ def commit_state(state: dict, state_path: str):
|
||||
|
||||
|
||||
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(
|
||||
f"Client configuration file not found at: {config_path}\n"
|
||||
f"Generate one from the server using: python Server.py --create-client-config --client-out {config_path}"
|
||||
)
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
with open(actual_config_path, "r", encoding="utf-8") as 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:
|
||||
@@ -301,6 +382,8 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
|
||||
hub_url = None
|
||||
if enrollment_secret:
|
||||
hermes_host = config.get("hermes_host", server_host)
|
||||
if hermes_host in ("0.0.0.0", "::", None):
|
||||
hermes_host = server_host if server_host not in ("0.0.0.0", "::", None) else "127.0.0.1"
|
||||
hermes_port = config.get("hermes_port", 8443)
|
||||
hub_url = f"http://{hermes_host}:{hermes_port}"
|
||||
try:
|
||||
|
||||
+187
-9
@@ -654,16 +654,160 @@ def sign_ca_csr_with_seats(
|
||||
return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||
|
||||
|
||||
def enroll_and_sync_license_ws(
|
||||
cert_dir: str = "certs",
|
||||
license_id: Optional[str] = None,
|
||||
license_authority_ws_url: str = "wss://logar.eibl.tech/ws/license",
|
||||
server_fingerprint: str = "",
|
||||
hostname: str = "",
|
||||
comm_cert_pem: Optional[str] = None,
|
||||
master_root_ca_pem: Optional[str] = None,
|
||||
seats_taken: Optional[int] = None,
|
||||
timeout: int = 15,
|
||||
verify_tls: bool = True
|
||||
) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
|
||||
"""
|
||||
Connects to the Master License Authority via WebSocket protocol, authenticating and
|
||||
submitting the server Sub-CA CSR to receive a cryptographically signed Sub-CA license
|
||||
certificate with embedded seat quotas.
|
||||
"""
|
||||
os.makedirs(cert_dir, exist_ok=True)
|
||||
ca_key_path = os.path.join(cert_dir, "ca.key")
|
||||
ca_cert_path = os.path.join(cert_dir, "ca.crt")
|
||||
|
||||
# 1. Load or generate client Sub-CA private key
|
||||
if os.path.exists(ca_key_path):
|
||||
with open(ca_key_path, "r", encoding="utf-8") as f:
|
||||
ca_key_pem = f.read()
|
||||
ca_key = serialization.load_pem_private_key(ca_key_pem.encode("utf-8"), password=None)
|
||||
else:
|
||||
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
||||
ca_key_pem = ca_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
).decode("utf-8")
|
||||
with open(ca_key_path, "w", encoding="utf-8") as f:
|
||||
f.write(ca_key_pem)
|
||||
|
||||
# 2. Build Sub-CA CSR
|
||||
csr_common_name = f"LOGAR-Server-CA-{license_id or 'Node'}"
|
||||
subject = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "AT"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LOGAR Subordinate Node"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, csr_common_name),
|
||||
])
|
||||
csr = (
|
||||
x509.CertificateSigningRequestBuilder()
|
||||
.subject_name(subject)
|
||||
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
|
||||
.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
key_encipherment=False,
|
||||
key_cert_sign=True,
|
||||
crl_sign=True,
|
||||
content_commitment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
),
|
||||
critical=True
|
||||
)
|
||||
.sign(ca_key, hashes.SHA256())
|
||||
)
|
||||
csr_pem = csr.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||
|
||||
# 3. Connect via WebSocket
|
||||
try:
|
||||
from websockets.sync.client import connect as ws_connect
|
||||
except ImportError:
|
||||
return False, "websockets library not available", None
|
||||
|
||||
ssl_context = None
|
||||
if license_authority_ws_url.startswith("wss://"):
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||
if not verify_tls:
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
elif master_root_ca_pem:
|
||||
ssl_context.load_verify_locations(cadata=master_root_ca_pem)
|
||||
else:
|
||||
upstream_path = os.path.join(cert_dir, "upstream_ca.crt")
|
||||
if os.path.exists(upstream_path):
|
||||
ssl_context.load_verify_locations(cafile=upstream_path)
|
||||
|
||||
try:
|
||||
with ws_connect(
|
||||
license_authority_ws_url,
|
||||
ssl=ssl_context,
|
||||
open_timeout=timeout,
|
||||
close_timeout=timeout
|
||||
) as ws:
|
||||
payload = {
|
||||
"action": "enroll_or_sync",
|
||||
"license_id": license_id,
|
||||
"communication_cert": comm_cert_pem,
|
||||
"server_csr": csr_pem,
|
||||
"server_fingerprint": server_fingerprint,
|
||||
"hostname": hostname
|
||||
}
|
||||
if seats_taken is not None:
|
||||
payload["seats_taken"] = seats_taken
|
||||
ws.send(json.dumps(payload))
|
||||
raw_reply = ws.recv(timeout=timeout)
|
||||
reply = json.loads(raw_reply)
|
||||
|
||||
if reply.get("status") != "success":
|
||||
return False, f"WebSocket licensing error: {reply.get('message', raw_reply)}", None
|
||||
|
||||
new_ca_pem = reply.get("ca_cert")
|
||||
if not new_ca_pem:
|
||||
return False, "License response missing 'ca_cert'", None
|
||||
|
||||
# Backup old ca.crt if present
|
||||
if os.path.exists(ca_cert_path):
|
||||
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
|
||||
try:
|
||||
os.replace(ca_cert_path, f"{ca_cert_path}.{ts}.bak")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with open(ca_cert_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_ca_pem)
|
||||
|
||||
upstream_ca = reply.get("master_root_ca") or master_root_ca_pem
|
||||
if upstream_ca:
|
||||
with open(os.path.join(cert_dir, "upstream_ca.crt"), "w", encoding="utf-8") as f:
|
||||
f.write(upstream_ca)
|
||||
|
||||
lic_info = extract_license_info_from_cert(new_ca_pem)
|
||||
return True, f"License {license_id} successfully enrolled via WebSocket ({reply.get('seats')} seats)", {
|
||||
"license_id": license_id,
|
||||
"seats": reply.get("seats"),
|
||||
"ca_cert_pem": new_ca_pem,
|
||||
"master_root_ca_pem": upstream_ca,
|
||||
"license_info": lic_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return False, f"WebSocket enrollment failed: {str(e)}", None
|
||||
|
||||
|
||||
def import_license_package(
|
||||
lic_content_or_path: str,
|
||||
cert_dir: str = "certs"
|
||||
cert_dir: str = "certs",
|
||||
server_fingerprint: str = "",
|
||||
hostname: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Imports and validates a license package or certificate.
|
||||
Supports:
|
||||
1. Activated JSON .lic package containing 'ca_cert' and optional 'master_root_ca'
|
||||
2. Raw PEM Subordinate CA certificate
|
||||
3. File path pointing to either of the above
|
||||
1. Bootstrap JSON .lic package (auto-enrolls via WebSocket to receive signed CA cert)
|
||||
2. Activated JSON .lic package containing 'ca_cert' and optional 'master_root_ca'
|
||||
3. Raw PEM Subordinate CA certificate
|
||||
4. File path pointing to any of the above
|
||||
Saves the signed CA certificate to cert_dir/ca.crt (backing up any old ca.crt),
|
||||
and if 'master_root_ca' is present, saves it to cert_dir/upstream_ca.crt for mTLS trust.
|
||||
Returns extracted license attributes and PEM contents.
|
||||
@@ -681,10 +825,35 @@ def import_license_package(
|
||||
if trimmed.startswith("{"):
|
||||
try:
|
||||
data = json.loads(trimmed)
|
||||
ca_pem = data.get("ca_cert") or data.get("cert_pem")
|
||||
master_root_ca_pem = data.get("master_root_ca") or data.get("upstream_ca")
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to parse JSON .lic package: {e}")
|
||||
|
||||
# If this is a bootstrap license package, auto-enroll over WebSocket
|
||||
if data.get("format") == "LOGAR_BOOTSTRAP_LICENSE" or (data.get("communication_cert") and not data.get("ca_cert")):
|
||||
lic_id = data.get("license_id")
|
||||
ws_url = data.get("license_authority", "wss://logar.eibl.tech/ws/license")
|
||||
comm_cert = data.get("communication_cert")
|
||||
master_ca = data.get("master_root_ca")
|
||||
|
||||
if master_ca:
|
||||
with open(os.path.join(cert_dir, "upstream_ca.crt"), "w", encoding="utf-8") as f:
|
||||
f.write(master_ca)
|
||||
|
||||
ok, msg, res = enroll_and_sync_license_ws(
|
||||
cert_dir=cert_dir,
|
||||
license_id=lic_id,
|
||||
license_authority_ws_url=ws_url,
|
||||
server_fingerprint=server_fingerprint or data.get("server_fingerprint", ""),
|
||||
hostname=hostname or data.get("hostname", ""),
|
||||
comm_cert_pem=comm_cert,
|
||||
master_root_ca_pem=master_ca
|
||||
)
|
||||
if not ok or not res:
|
||||
raise ValueError(msg)
|
||||
return res
|
||||
|
||||
ca_pem = data.get("ca_cert") or data.get("cert_pem")
|
||||
master_root_ca_pem = data.get("master_root_ca") or data.get("upstream_ca")
|
||||
elif "-----BEGIN CERTIFICATE-----" in trimmed:
|
||||
ca_pem = trimmed
|
||||
|
||||
@@ -731,13 +900,17 @@ def renew_ca_cert_mtls(
|
||||
upstream_ca_pem: Optional[str] = None,
|
||||
requested_seats: Optional[int] = None,
|
||||
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,
|
||||
verify_tls: bool = True
|
||||
) -> Tuple[bool, str, Optional[str]]:
|
||||
"""
|
||||
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.
|
||||
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.
|
||||
Returns (success, message, new_ca_cert_pem).
|
||||
"""
|
||||
@@ -755,10 +928,15 @@ def renew_ca_cert_mtls(
|
||||
current_pem = f.read()
|
||||
info = extract_license_info_from_cert(current_pem)
|
||||
|
||||
effective_seats_taken = seats_taken if seats_taken is not None else active_seats
|
||||
payload_dict = {
|
||||
"license_id": info.get("license_id"),
|
||||
"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:
|
||||
payload_dict["requested_seats"] = requested_seats
|
||||
@@ -779,7 +957,7 @@ def renew_ca_cert_mtls(
|
||||
ssl_ctx.check_hostname = False
|
||||
ssl_ctx.verify_mode = ssl.CERT_NONE
|
||||
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")
|
||||
if os.path.exists(upstream_ca_path) and verify_tls and not upstream_ca_pem:
|
||||
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
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
# Ensure src/ is importable
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
SRC_DIR = os.path.join(ROOT_DIR, "src")
|
||||
@@ -15,32 +20,68 @@ if SRC_DIR not in sys.path:
|
||||
sys.path.insert(0, SRC_DIR)
|
||||
|
||||
import server_enrollment as enrollment
|
||||
import License_Authority as authority
|
||||
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):
|
||||
self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp")
|
||||
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.db_path = os.path.join(self.test_dir, "license_auth.db")
|
||||
self.server_db = os.path.join(self.test_dir, "server_state.db")
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(self.test_dir):
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
def test_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):
|
||||
"""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(
|
||||
cert_dir=self.server_cert_dir,
|
||||
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, "logar_server.lic")))
|
||||
|
||||
def test_authority_issue_sub_ca_with_exact_seats(self):
|
||||
"""Verifies that License Authority signs CSR, embeds exact seat quota via custom OID, and verifies in DB."""
|
||||
def test_license_info_extraction_and_custom_oids(self):
|
||||
"""Verifies extraction of embedded custom seat OIDs and license attributes from certificates."""
|
||||
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
||||
result = authority.issue_license_from_csr(
|
||||
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
|
||||
)
|
||||
sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=42, license_id="LIC-ABC-4242")
|
||||
|
||||
self.assertEqual(result["status"], "success")
|
||||
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"])
|
||||
info = enrollment.extract_license_info_from_cert(sub_pem)
|
||||
self.assertTrue(info["valid"])
|
||||
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.assertFalse(info["is_expired"])
|
||||
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):
|
||||
"""Verifies that importing a signed CA cert updates the server's seat quota in DB and memory."""
|
||||
# 1. Generate CSR on server side
|
||||
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
||||
|
||||
# 2. Authority signs it with 50 seats
|
||||
signed = authority.issue_license_from_csr(
|
||||
csr_pem=csr_pem,
|
||||
seats=50,
|
||||
db_path=self.db_path,
|
||||
cert_dir=self.master_ca_dir
|
||||
)
|
||||
# 2. Mock sign it with 50 seats
|
||||
sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=50, license_id="LIC-IMPORT-50")
|
||||
|
||||
# 3. Setup mock server state and DB
|
||||
Server.init_db(self.server_db, max_seats=5)
|
||||
@@ -160,16 +139,16 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
||||
# 5. Save the signed CA certificate to cert_dir/ca.crt
|
||||
ca_path = os.path.join(self.server_cert_dir, "ca.crt")
|
||||
with open(ca_path, "w", encoding="utf-8") as f:
|
||||
f.write(signed["ca_cert"])
|
||||
f.write(sub_pem)
|
||||
|
||||
# 6. Extract summary after import
|
||||
summary_after = Server.get_license_summary()
|
||||
self.assertEqual(summary_after["max_seats"], 50)
|
||||
self.assertEqual(summary_after["license_id"], signed["license_id"])
|
||||
self.assertEqual(summary_after["license_id"], "LIC-IMPORT-50")
|
||||
self.assertEqual(summary_after["status"], "ACTIVE")
|
||||
|
||||
def test_server_client_revocation_and_reactivation(self):
|
||||
"""Verifies revocation and reactivation of edge client seats."""
|
||||
"""Verifies revocation and reactivation of edge client seats on the server hub."""
|
||||
Server.init_db(self.server_db, max_seats=2)
|
||||
Server.SERVER_STATE["config"] = {
|
||||
"db_path": self.server_db,
|
||||
@@ -202,81 +181,29 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
||||
self.assertEqual(summary2["active_seats"], 2)
|
||||
self.assertEqual(summary2["available_seats"], 0)
|
||||
|
||||
def test_activated_lic_package_import_and_renewal_seat_increase(self):
|
||||
"""Verifies that an activated .lic package can be imported, saving upstream_ca, and renewal expands seats."""
|
||||
# 1. Generate CSR on server side
|
||||
def test_activated_lic_package_handling(self):
|
||||
"""Verifies that an activated .lic package structure can be processed and verified."""
|
||||
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
|
||||
sub_pem, root_pem = _create_mock_signed_license_cert(csr_pem, seats=20, license_id="LIC-ACT-20")
|
||||
|
||||
# 2. Authority signs it with 20 seats and produces activated .lic
|
||||
issued = authority.issue_license_from_csr(
|
||||
csr_pem=csr_pem,
|
||||
seats=20,
|
||||
server_fingerprint="FP_TEST_HUB",
|
||||
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,
|
||||
lic_package = {
|
||||
"format": "LOGAR_ACTIVATED_LICENSE",
|
||||
"version": "2.0.1",
|
||||
"license_id": "LIC-ACT-20",
|
||||
"seats": 20,
|
||||
"server_fingerprint": "FP_TEST_HUB",
|
||||
"server_name": "Test-Hub",
|
||||
"license_server_url": "https://logar.eibl.tech"
|
||||
"ca_cert": sub_pem,
|
||||
"master_root_ca": root_pem,
|
||||
"days_valid": 365
|
||||
}
|
||||
summary = Server.get_license_summary()
|
||||
self.assertEqual(summary["max_seats"], 20)
|
||||
self.assertEqual(lic_package["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)
|
||||
with open(os.path.join(self.server_cert_dir, "ca.crt"), "r", encoding="utf-8") as f:
|
||||
current_ca_pem = f.read()
|
||||
|
||||
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)
|
||||
# Verify certificate parses
|
||||
info = enrollment.extract_license_info_from_cert(lic_package["ca_cert"])
|
||||
self.assertEqual(info["seats"], 20)
|
||||
self.assertEqual(info["license_id"], "LIC-ACT-20")
|
||||
|
||||
def test_webui_rendering(self):
|
||||
"""Verifies that the WebUI HTML contains key components, stats, and download triggers."""
|
||||
@@ -294,10 +221,11 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
|
||||
|
||||
response = Server.serve_webui()
|
||||
html = response.body.decode("utf-8")
|
||||
self.assertIn("LOGAR Fleet Hub & License Management", html.replace("&", "&"))
|
||||
self.assertIn("Export .lic Request", html)
|
||||
self.assertIn("Import License (.lic / ca.crt)", html)
|
||||
self.assertIn("Sync & Renew via mTLS", html.replace("&", "&"))
|
||||
self.assertIn("LOGAR Fleet Hub", html)
|
||||
self.assertNotIn("LOGAR Fleet Hub & License Management", html)
|
||||
self.assertNotIn("Export .lic Request", html)
|
||||
self.assertNotIn("Import License (.lic / ca.crt)", html)
|
||||
self.assertIn("Sync & Renew via mTLS", html)
|
||||
self.assertIn("logar.eibl.tech", html)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||
@@ -26,7 +27,12 @@ class TestLinuxClientComponent(unittest.TestCase):
|
||||
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
|
||||
hashes=[HashAlgorithm.SHA256],
|
||||
ciphers=[SymmetricKeyAlgorithm.AES256],
|
||||
compression=[CompressionAlgorithm.Uncompressed]
|
||||
compression=[
|
||||
CompressionAlgorithm.ZIP,
|
||||
CompressionAlgorithm.ZLIB,
|
||||
CompressionAlgorithm.BZ2,
|
||||
CompressionAlgorithm.Uncompressed
|
||||
]
|
||||
)
|
||||
self.server_priv = key
|
||||
self.server_pub = key.pubkey
|
||||
|
||||
+13
-9
@@ -109,7 +109,9 @@ def run_tests():
|
||||
print(f"[OK] Client enrolled successfully. Certificates stored in {cert_dir}")
|
||||
|
||||
# 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:
|
||||
clients_data = json.loads(resp.read().decode("utf-8"))
|
||||
assert clients_data["active_seats"] >= 1
|
||||
@@ -146,8 +148,8 @@ def run_tests():
|
||||
resp_bytes.extend(chunk)
|
||||
return json.loads(resp_bytes.decode("utf-8"))
|
||||
|
||||
print("\n=== [3] Testing Temporal Persistence & 4-Run Rule for Warnings over mTLS ===")
|
||||
test_signature = "TestServiceDegraded"
|
||||
print("\n=== [3] Testing Temporal Persistence & Direct Anomaly Verification over mTLS ===")
|
||||
test_signature = f"TestServiceDegraded_{int(time.time())}"
|
||||
candidate_log = [{
|
||||
"server": client_id,
|
||||
"os_type": "linux",
|
||||
@@ -161,10 +163,10 @@ def run_tests():
|
||||
assert resp.get("status") == "success", f"Run {run_num} failed: {resp}"
|
||||
promoted = resp.get("promoted_verified", 0)
|
||||
print(f"[Run {run_num}/4] Ingested successfully via mTLS. Promoted to verified: {promoted}")
|
||||
if run_num < 4:
|
||||
assert promoted == 0, f"Expected 0 promoted on run {run_num} for warning, got {promoted}"
|
||||
if run_num == 1:
|
||||
assert promoted == 1, f"Expected 1 promoted on run 1 for warning, got {promoted}"
|
||||
else:
|
||||
assert promoted == 1, f"Expected 1 promoted on run 4 for warning, got {promoted}"
|
||||
assert promoted == 0, f"Expected 0 newly promoted on run {run_num} (already verified), got {promoted}"
|
||||
|
||||
# Inspect SQLite database directly
|
||||
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
|
||||
@@ -178,10 +180,10 @@ def run_tests():
|
||||
print(f"[DB Verification] Issue '{test_signature}' -> run_count: {run_count}, status: {status}")
|
||||
assert run_count >= 4, f"Expected run_count >= 4, got {run_count}"
|
||||
assert status == "VERIFIED", f"Expected status 'VERIFIED', got {status}"
|
||||
print("[OK] 4-Run Rule verified: Warning promoted to VERIFIED anomaly on 4th run 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 ===")
|
||||
error_signature = "TestServiceCrashImmediate"
|
||||
error_signature = f"TestServiceCrashImmediate_{int(time.time())}"
|
||||
error_log = [{
|
||||
"server": client_id,
|
||||
"os_type": "linux",
|
||||
@@ -207,7 +209,9 @@ def run_tests():
|
||||
print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!")
|
||||
|
||||
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:
|
||||
assert response.status == 200, f"Expected 200, got {response.status}"
|
||||
hermes_data = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
+130
-16
@@ -75,7 +75,7 @@ class TestServerComponent(unittest.TestCase):
|
||||
if os.path.exists(client_out):
|
||||
os.remove(client_out)
|
||||
|
||||
def test_4_run_rule_and_12h_window(self):
|
||||
def test_direct_hermes_anomaly_and_12h_window(self):
|
||||
Server.init_db(self.test_db)
|
||||
log_entry = {
|
||||
"server": "app-worker-01.corp.local",
|
||||
@@ -89,23 +89,24 @@ class TestServerComponent(unittest.TestCase):
|
||||
"logs": [log_entry]
|
||||
}
|
||||
|
||||
# Runs 1 to 3: WARNING should remain TRANSIENT
|
||||
for run_idx in range(1, 4):
|
||||
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
|
||||
self.assertEqual(res["status"], "success")
|
||||
self.assertEqual(res["promoted_verified"], 0)
|
||||
# Run 1: WARNING is immediately VERIFIED for Hermes AI reasoning
|
||||
res1 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
|
||||
self.assertEqual(res1["status"], "success")
|
||||
self.assertEqual(res1["promoted_verified"], 1)
|
||||
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
self.assertEqual(row[0], 3)
|
||||
self.assertEqual(row[1], "TRANSIENT")
|
||||
self.assertEqual(row[0], 1)
|
||||
self.assertEqual(row[1], "VERIFIED")
|
||||
|
||||
# Run 4: promotes WARNING to VERIFIED!
|
||||
res4 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
|
||||
self.assertEqual(res4["promoted_verified"], 1)
|
||||
# Runs 2 to 4: Increments consecutive run count while remaining VERIFIED
|
||||
for run_idx in range(2, 5):
|
||||
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
|
||||
self.assertEqual(res["status"], "success")
|
||||
self.assertEqual(res["promoted_verified"], 0)
|
||||
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
c = conn.cursor()
|
||||
@@ -130,7 +131,7 @@ class TestServerComponent(unittest.TestCase):
|
||||
}
|
||||
|
||||
# Run 1: ERROR must immediately promote to VERIFIED
|
||||
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
|
||||
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
|
||||
self.assertEqual(res["status"], "success")
|
||||
self.assertEqual(res["promoted_verified"], 1)
|
||||
|
||||
@@ -156,7 +157,7 @@ class TestServerComponent(unittest.TestCase):
|
||||
{"server": "app-worker-01", "signature": "SigTrace", "severity": "TRACE", "message": "Trace msg", "os_type": "linux"}
|
||||
]
|
||||
}
|
||||
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
|
||||
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
|
||||
self.assertEqual(res["status"], "success")
|
||||
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
@@ -171,10 +172,10 @@ class TestServerComponent(unittest.TestCase):
|
||||
self.assertNotIn("SigDebug", rows)
|
||||
self.assertNotIn("SigTrace", rows)
|
||||
|
||||
# SigErr is immediately VERIFIED; SigWarn and SigInfo are TRANSIENT on run 1
|
||||
# All actionable issues (ERROR, WARNING, INFO) are promoted to VERIFIED immediately for Hermes reasoning
|
||||
self.assertEqual(rows["SigErr"], "VERIFIED")
|
||||
self.assertEqual(rows["SigWarn"], "TRANSIENT")
|
||||
self.assertEqual(rows["SigInfo"], "TRANSIENT")
|
||||
self.assertEqual(rows["SigWarn"], "VERIFIED")
|
||||
self.assertEqual(rows["SigInfo"], "VERIFIED")
|
||||
|
||||
def test_license_schema_and_pki_generation(self):
|
||||
secret = "test-secret-12345"
|
||||
@@ -328,6 +329,119 @@ class TestServerComponent(unittest.TestCase):
|
||||
if os.path.exists(test_cert_dir):
|
||||
shutil.rmtree(test_cert_dir, ignore_errors=True)
|
||||
|
||||
def test_user_auth_and_session(self):
|
||||
"""Tests admin password hashing, verification, session tokens, and password change."""
|
||||
Server.init_db(self.test_db)
|
||||
|
||||
# Verify default admin credentials
|
||||
self.assertTrue(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
|
||||
self.assertFalse(Server.verify_user_credentials("admin", "wrong_password", db_path=self.test_db))
|
||||
self.assertFalse(Server.verify_user_credentials("nonexistent", "password", db_path=self.test_db))
|
||||
|
||||
# Test session token generation & validation
|
||||
token = Server.generate_session_token("admin")
|
||||
self.assertEqual(Server.verify_session_token(token), "admin")
|
||||
self.assertIsNone(Server.verify_session_token("invalid.token.structure"))
|
||||
|
||||
# Test changing password
|
||||
Server.change_user_password("admin", "new_secure_pass123", db_path=self.test_db)
|
||||
self.assertFalse(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
|
||||
self.assertTrue(Server.verify_user_credentials("admin", "new_secure_pass123", db_path=self.test_db))
|
||||
|
||||
def test_hermes_tokens_and_auth(self):
|
||||
"""Tests Hermes Bearer token creation, verification, revocation, and deletion."""
|
||||
Server.init_db(self.test_db)
|
||||
|
||||
# Test default Hermes token was created on init
|
||||
tokens = Server.list_hermes_tokens(db_path=self.test_db)
|
||||
self.assertEqual(len(tokens), 1)
|
||||
self.assertEqual(tokens[0]["status"], "active")
|
||||
default_token = tokens[0]["token"]
|
||||
self.assertTrue(Server.verify_hermes_token(default_token, db_path=self.test_db))
|
||||
|
||||
# Test creating new token
|
||||
new_tok = Server.create_hermes_token(name="Hermes Autonomous Agent 01", db_path=self.test_db)
|
||||
self.assertIn("token", new_tok)
|
||||
self.assertEqual(new_tok["name"], "Hermes Autonomous Agent 01")
|
||||
self.assertTrue(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||
|
||||
# Test revoking token
|
||||
self.assertTrue(Server.revoke_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||
self.assertFalse(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||
|
||||
# Test deleting token
|
||||
self.assertTrue(Server.delete_hermes_token(new_tok["token"], db_path=self.test_db))
|
||||
tokens_after = Server.list_hermes_tokens(db_path=self.test_db)
|
||||
self.assertEqual(len(tokens_after), 1)
|
||||
|
||||
def test_client_profile_export(self):
|
||||
"""Tests client profile dictionary generation and export formatting."""
|
||||
cfg = Server.load_or_init_config(self.test_config)
|
||||
profile = Server.generate_client_profile_dict(
|
||||
server_host="logar-hub.corp.internal",
|
||||
server_port=9443,
|
||||
client_id="worker-node-101",
|
||||
config_dict=cfg
|
||||
)
|
||||
self.assertEqual(profile["server_host"], "logar-hub.corp.internal")
|
||||
self.assertEqual(profile["server_port"], 9443)
|
||||
self.assertEqual(profile["client_id"], "worker-node-101")
|
||||
self.assertEqual(profile["server_fingerprint"], cfg["server_fingerprint"])
|
||||
self.assertEqual(profile["server_public_key"], cfg["public_key"])
|
||||
self.assertEqual(profile["enrollment_secret"], cfg["enrollment_secret"])
|
||||
self.assertEqual(profile["format"], "LOGAR_CLIENT_PROFILE")
|
||||
|
||||
def test_client_seat_management_and_relative_time(self):
|
||||
"""Tests client listing, seat approval, revocation, deletion, and relative timestamp helper."""
|
||||
Server.init_db(self.test_db, enrollment_secret="test-sec-123", max_seats=5)
|
||||
Server.SERVER_STATE["config"] = {"db_path": self.test_db}
|
||||
|
||||
# Populate test clients
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
now = datetime.now(timezone.utc)
|
||||
five_min_ago = (now - timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
two_hours_ago = (now - timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-win', 'WIN-PROD-01', 'windows', 'FP_W1', 'active', ?)", (five_min_ago,))
|
||||
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-lin', 'LIN-PROD-01', 'linux', 'FP_L1', 'active', ?)", (two_hours_ago,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# 1. Test listing
|
||||
client_data = Server.list_clients()
|
||||
self.assertEqual(client_data["active_seats"], 2)
|
||||
self.assertEqual(client_data["max_seats"], 5)
|
||||
self.assertEqual(len(client_data["clients"]), 2)
|
||||
self.assertEqual(client_data["clients"][0]["last_activity_relative"], "5m ago")
|
||||
|
||||
# 2. Test revoking
|
||||
rev_req = Server.ClientActionRequest(client_id="c-win")
|
||||
rev_res = Server.revoke_client(rev_req)
|
||||
self.assertEqual(rev_res["status"], "revoked")
|
||||
client_data = Server.list_clients()
|
||||
self.assertEqual(client_data["active_seats"], 1)
|
||||
|
||||
# 3. Test approving / reactivating
|
||||
app_res = Server.approve_client(rev_req)
|
||||
self.assertEqual(app_res["status"], "active")
|
||||
client_data = Server.list_clients()
|
||||
self.assertEqual(client_data["active_seats"], 2)
|
||||
|
||||
# 4. Test deleting client record
|
||||
del_res = Server.delete_client(rev_req)
|
||||
self.assertEqual(del_res["status"], "deleted")
|
||||
client_data = Server.list_clients()
|
||||
self.assertEqual(len(client_data["clients"]), 1)
|
||||
self.assertEqual(client_data["active_seats"], 1)
|
||||
|
||||
# 5. Test format_relative_time edge cases
|
||||
self.assertEqual(Server.format_relative_time(None), "Never")
|
||||
self.assertEqual(Server.format_relative_time(""), "Never")
|
||||
self.assertEqual(Server.format_relative_time("invalid-date-string"), "invalid-date-str")
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||
@@ -26,7 +27,12 @@ class TestWinClientComponent(unittest.TestCase):
|
||||
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
|
||||
hashes=[HashAlgorithm.SHA256],
|
||||
ciphers=[SymmetricKeyAlgorithm.AES256],
|
||||
compression=[CompressionAlgorithm.Uncompressed]
|
||||
compression=[
|
||||
CompressionAlgorithm.ZIP,
|
||||
CompressionAlgorithm.ZLIB,
|
||||
CompressionAlgorithm.BZ2,
|
||||
CompressionAlgorithm.Uncompressed
|
||||
]
|
||||
)
|
||||
self.server_priv = key
|
||||
self.server_pub = key.pubkey
|
||||
|
||||
Reference in New Issue
Block a user