Compare commits
35
Commits
v1.0.1
..
cb4c763e0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb4c763e0e | ||
|
|
84579d8719 | ||
|
|
08aa4edfb1 | ||
|
|
e4f6a9295b | ||
|
|
7f6bf4442f | ||
|
|
db80e7f5a2 | ||
|
|
1562285034 | ||
|
|
74942d4c00 | ||
|
|
874d693dac | ||
|
|
15bfb4940b | ||
|
|
364fefea47 | ||
|
|
9624935a81 | ||
|
|
867271e8b7 | ||
|
|
205d0cfbad | ||
|
|
d37191e302 | ||
|
|
35a736dacb | ||
|
|
f871344da4 | ||
|
|
e1dbe32063 | ||
|
|
98a227a234 | ||
|
|
355c6e1bc0 | ||
|
|
907616511b | ||
|
|
939fa4270a | ||
|
|
1f4bf3219b | ||
|
|
df03c52a05 | ||
|
|
d63a623763 | ||
|
|
7ed264db5a | ||
|
|
6fec838344 | ||
|
|
4c160924f7 | ||
|
|
99be50ebc6 | ||
|
|
85f0d94805 | ||
|
|
1a18c4c079 | ||
|
|
a770e24f26 | ||
|
|
f7ebc6c0a1 | ||
|
|
86649f796d | ||
|
|
78fc2ac8c5 |
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
tags-ignore:
|
tags-ignore:
|
||||||
- 'v*'
|
- '*'
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ jobs:
|
|||||||
apt-get install -y python3 python3-pip python3-venv curl
|
apt-get install -y python3 python3-pip python3-venv curl
|
||||||
fi
|
fi
|
||||||
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
||||||
pip3 install -r requirements.txt --break-system-packages || pip3 install -r requirements.txt
|
pip3 install -r compilation/requirements.txt --break-system-packages || pip3 install -r compilation/requirements.txt
|
||||||
|
|
||||||
- name: Verify Python Syntax
|
- name: Verify Python Syntax
|
||||||
run: |
|
run: |
|
||||||
python3 -m py_compile Server.py Win_Client.py Linux_Client.py package_dist.py upload_release.py test_pipeline.py tests/*.py
|
python3 -m py_compile src/Server.py src/Win_Client.py src/Linux_Client.py compilation/package_dist.py compilation/upload_release.py tests/test_pipeline.py tests/*.py
|
||||||
|
|
||||||
- name: Run Component Unit Tests
|
- name: Run Component Unit Tests
|
||||||
run: |
|
run: |
|
||||||
@@ -40,10 +40,10 @@ jobs:
|
|||||||
rm -f server_config.json client_config.json logar_state.db client_state.json
|
rm -f server_config.json client_config.json logar_state.db client_state.json
|
||||||
|
|
||||||
# 1. Initialize server config and export client configuration
|
# 1. Initialize server config and export client configuration
|
||||||
python3 Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
|
python3 src/Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
|
||||||
|
|
||||||
# 2. Launch LOGAR server in the background
|
# 2. Launch LOGAR server in the background
|
||||||
python3 Server.py &
|
python3 src/Server.py &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
echo "[*] Server launched in background with PID $SERVER_PID"
|
echo "[*] Server launched in background with PID $SERVER_PID"
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Execute end-to-end integration test
|
# 4. Execute end-to-end integration test
|
||||||
python3 test_pipeline.py
|
python3 tests/test_pipeline.py
|
||||||
|
|
||||||
# 5. Cleanly terminate background server
|
# 5. Cleanly terminate background server
|
||||||
kill $SERVER_PID || true
|
kill $SERVER_PID || true
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
name: Release Linux Binaries
|
name: Release Linux Binaries
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
release:
|
||||||
|
types: [published, created]
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- '*'
|
||||||
|
- '**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Release tag (e.g. v1.0.1)'
|
description: 'Release tag (optional)'
|
||||||
required: false
|
required: false
|
||||||
default: 'v1.0.1'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-linux:
|
release-linux:
|
||||||
@@ -26,17 +28,17 @@ jobs:
|
|||||||
apt-get install -y python3 python3-pip python3-venv binutils zip
|
apt-get install -y python3 python3-pip python3-venv binutils zip
|
||||||
fi
|
fi
|
||||||
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
|
||||||
pip3 install pyinstaller -r requirements.txt --break-system-packages || pip3 install pyinstaller -r requirements.txt
|
pip3 install pyinstaller -r compilation/requirements.txt --break-system-packages || pip3 install pyinstaller -r compilation/requirements.txt
|
||||||
|
|
||||||
- name: Compile Standalone Linux Binaries
|
- name: Compile Standalone Linux Binaries
|
||||||
run: |
|
run: |
|
||||||
python3 package_dist.py --target linux
|
python3 compilation/package_dist.py --target linux
|
||||||
|
|
||||||
- name: Publish Linux Release Assets
|
- name: Publish Linux Release Assets
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
GITEA_REPOSITORY: ${{ github.repository }}
|
||||||
GITEA_REF_NAME: ${{ inputs.tag || github.ref_name }}
|
GITEA_REF_NAME: ${{ github.event.release.tag_name || inputs.tag || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
python3 upload_release.py --skip-build
|
python3 compilation/upload_release.py --skip-build
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
name: Release Windows Binaries
|
name: Release Windows Binaries
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
release:
|
||||||
|
types: [published, created]
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- '*'
|
||||||
|
- '**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Release tag (e.g. v1.0.1)'
|
description: 'Release tag (optional)'
|
||||||
required: false
|
required: false
|
||||||
default: 'v1.0.1'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-windows:
|
release-windows:
|
||||||
@@ -35,7 +37,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
& $py -m pip install --upgrade pip
|
& $py -m pip install --upgrade pip
|
||||||
& $py -m pip install pyinstaller -r requirements.txt
|
& $py -m pip install pyinstaller -r compilation/requirements.txt
|
||||||
|
|
||||||
- name: Compile Standalone Windows Binaries
|
- name: Compile Standalone Windows Binaries
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -46,7 +48,7 @@ jobs:
|
|||||||
$py = "py -3.12"
|
$py = "py -3.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& $py package_dist.py --target windows
|
& $py compilation/package_dist.py --target windows
|
||||||
|
|
||||||
- name: Publish Windows Release Assets
|
- name: Publish Windows Release Assets
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -54,7 +56,7 @@ jobs:
|
|||||||
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
GITEA_REPOSITORY: ${{ github.repository }}
|
||||||
GITEA_REF_NAME: ${{ inputs.tag || github.ref_name }}
|
GITEA_REF_NAME: ${{ github.event.release.tag_name || inputs.tag || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
$py = "python"
|
$py = "python"
|
||||||
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
|
||||||
@@ -62,4 +64,4 @@ jobs:
|
|||||||
$py = "py -3.12"
|
$py = "py -3.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& $py upload_release.py --skip-build
|
& $py compilation/upload_release.py --skip-build
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# LOGAR: Edge-Thin Log Analysis & Temporal Verification System
|
# LOGAR: Edge-Thin Log Analysis & Temporal Verification System
|
||||||
|
|
||||||
**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 OpenPGP encryption, authenticated TCP streaming, temporal persistence tracking across 12-hour evaluation windows, and an automated 4-run rule to filter out transient infrastructure blips before reporting verified anomalies to **Hermes**.
|
**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 OpenPGP encryption, authenticated TCP streaming, 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**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -27,14 +27,15 @@ Site agents running on Windows and Linux act strictly as lightweight forwarders:
|
|||||||
- **End-to-End Encryption**: Logs are encrypted using the server's OpenPGP public key before leaving the edge node.
|
- **End-to-End Encryption**: Logs are encrypted using the server's OpenPGP public key before leaving the edge node.
|
||||||
- **Secure TCP Sockets**: Ingestion occurs over low-overhead authenticated TCP sockets rather than bulky HTTP/HTTPS endpoints.
|
- **Secure TCP Sockets**: Ingestion occurs over low-overhead authenticated TCP sockets rather than bulky HTTP/HTTPS endpoints.
|
||||||
|
|
||||||
### 2. Cloud-Side Temporal Persistence
|
### 2. Cloud-Side Temporal Persistence & Severity Routing
|
||||||
The central Python/TCP hub handles the heavy lifting:
|
The central Python/TCP hub handles the heavy lifting:
|
||||||
- State tracking is managed centrally in SQLite (`logar_state.db`).
|
- State tracking is managed centrally in SQLite (`logar_state.db`).
|
||||||
- Candidate issues are evaluated over a **12-hour temporal evaluation window**.
|
- Candidate issues are evaluated over a **12-hour temporal evaluation window**.
|
||||||
- An issue must persist across **at least 4 consecutive runs / cycles** to be confirmed as a genuine system anomaly. Transient blips and sporadic spikes are filtered out automatically.
|
- **Warning Persistence (4-Run Rule)**: `WARNING` level issues must persist across **at least 4 consecutive runs / cycles** within the 12-hour window to be confirmed as genuine anomalies, automatically filtering out transient 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
|
### 3. Agentic Integration with Hermes
|
||||||
Instead of human engineers manually diving through noisy logs, **Hermes** ingests pre-filtered, 4-run validated anomalies directly from the cloud hub (`GET /api/hermes/report`), treating them as verified system artifacts to trigger precise team notifications.
|
Instead of human engineers manually diving through noisy 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 team notifications.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ graph TB
|
|||||||
- A cryptographically random authentication secret token (`auth_token`).
|
- A cryptographically random authentication secret token (`auth_token`).
|
||||||
- **Client Configuration Exporter**:
|
- **Client Configuration Exporter**:
|
||||||
```bash
|
```bash
|
||||||
python Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
|
python src/Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
|
||||||
```
|
```
|
||||||
Produces an anonymous client config containing only the server socket coordinates, authentication token, and the encryption-only public key & fingerprint.
|
Produces an anonymous client config containing only the server socket coordinates, authentication token, and the encryption-only public key & fingerprint.
|
||||||
- **Socket Protocol Framing**:
|
- **Socket Protocol Framing**:
|
||||||
@@ -106,16 +107,20 @@ graph TB
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Cloud-Side Temporal Persistence & 4-Run Rule
|
## Cloud-Side Temporal Persistence & 4-Run Rule
|
||||||
|
|
||||||
Incoming candidate logs are tracked in SQLite table `active_issues`:
|
Incoming candidate logs are tracked in SQLite table `active_issues`:
|
||||||
- **Issue Fingerprint**: Formatted as `{site_name}:{server}:{signature}`.
|
- **Issue Fingerprint**: Formatted as `{site_name}:{server}:{signature}`.
|
||||||
- **12-Hour Evaluation Window**:
|
- **12-Hour Evaluation Window**:
|
||||||
- When an issue is observed, the hub compares `(now - last_seen)`.
|
- When an issue is observed, the hub compares `(now - last_seen)`.
|
||||||
- If more than 12 hours have passed since the issue was last recorded, the previous window is expired and the cycle resets to `run_count = 1` with status `TRANSIENT`.
|
- If more than 12 hours have passed since the issue was last recorded, the previous window is expired and the cycle resets to `run_count = 1`.
|
||||||
- **4-Run Rule**:
|
- **4-Run Rule for Warnings**:
|
||||||
|
- The 4-run persistence threshold specifically applies to `WARNING` (and `INFO`) events to eliminate transient operational noise.
|
||||||
- For each distinct run batch, `run_count` increments.
|
- For each distinct run batch, `run_count` increments.
|
||||||
- Issues with `run_count < 4` are marked as `TRANSIENT` and ignored by downstream reporting.
|
- 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 status transitions to `VERIFIED`.
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -124,7 +129,7 @@ Incoming candidate logs are tracked in SQLite table `active_issues`:
|
|||||||
The server hub serves a REST reporting API (default port `8443`):
|
The server hub serves a REST reporting API (default port `8443`):
|
||||||
|
|
||||||
### `GET /api/hermes/report`
|
### `GET /api/hermes/report`
|
||||||
Returns exclusively **verified anomalies** that have satisfied the 4-run rule within the active 12-hour evaluation window:
|
Returns all **verified anomalies** (immediate critical errors and warnings verified after 4 consecutive runs within the 12-hour evaluation window):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
@@ -177,27 +182,37 @@ LOGAR/
|
|||||||
│ ├── ci.yml # Continuous Integration automated test suite (runs on every push)
|
│ ├── ci.yml # Continuous Integration automated test suite (runs on every push)
|
||||||
│ ├── release-linux.yml # Linux release workflow (compiles Server.bin, Linux_Client.bin, checksums)
|
│ ├── release-linux.yml # Linux release workflow (compiles Server.bin, Linux_Client.bin, checksums)
|
||||||
│ └── release-windows.yml # Windows release workflow (compiles Server.exe, Win_Client.exe, checksums)
|
│ └── release-windows.yml # Windows release workflow (compiles Server.exe, Win_Client.exe, checksums)
|
||||||
├── .gitignore # Ignore venv, caches, DBs, and private keys
|
├── compilation/ # Build, packaging, and release automation tools
|
||||||
├── requirements.txt # Unified dependencies
|
│ ├── package_dist.py # Multi-platform standalone binary packaging script
|
||||||
├── README.md # Comprehensive documentation
|
│ ├── requirements.txt # Unified project dependencies
|
||||||
├── Server.py # Central TCP server and Hermes API
|
│ └── upload_release.py # Direct Gitea REST API release asset publisher
|
||||||
├── Win_Client.py # Windows edge forwarder
|
├── src/ # Core application source modules
|
||||||
├── Linux_Client.py # Linux edge forwarder
|
│ ├── __init__.py
|
||||||
├── server_config.sample.json # Central server sample configuration
|
│ ├── Server.py # Central TCP server and Hermes API
|
||||||
├── package_dist.py # Multi-platform standalone binary packaging script
|
│ ├── Win_Client.py # Windows edge forwarder
|
||||||
├── upload_release.py # Direct Gitea REST API release asset publisher
|
│ └── Linux_Client.py # Linux edge forwarder
|
||||||
├── test_pipeline.py # End-to-end integration test
|
├── tests/ # Automated test suites
|
||||||
├── tests/ # Unified unit test suites
|
│ ├── test_linux_client.py # Linux client unit tests
|
||||||
|
│ ├── test_pipeline.py # End-to-end integration test
|
||||||
│ ├── test_server.py # Server unit tests
|
│ ├── test_server.py # Server unit tests
|
||||||
│ ├── test_win_client.py # Windows client unit tests
|
│ └── test_win_client.py # Windows client unit tests
|
||||||
│ └── test_linux_client.py # Linux client unit tests
|
├── .gitignore # Ignore venv, caches, DBs, and private keys
|
||||||
└── out/ # Edge forwarder deployment packages
|
├── README.md # Comprehensive documentation
|
||||||
├── win_client/
|
├── RELEASE_NOTES.md # Release history and changelog
|
||||||
│ ├── client_config.sample.json # Reference client configuration
|
├── server_config.sample.json # Central server sample configuration
|
||||||
│ └── README.md # Windows service installation & configuration guide
|
└── out/ # Standalone deployment documentation & sample configs
|
||||||
└── linux_client/
|
├── linux_server/
|
||||||
├── client_config.sample.json # Reference client configuration
|
│ ├── README.md # Linux systemd service installation & hub guide
|
||||||
└── README.md # Linux service installation & configuration guide
|
│ └── server_config.sample.json # Reference server configuration
|
||||||
|
├── win_server/
|
||||||
|
│ ├── README.md # Windows service (NSSM/Task Scheduler) installation guide
|
||||||
|
│ └── server_config.sample.json # Reference server configuration
|
||||||
|
├── linux_client/
|
||||||
|
│ ├── README.md # Linux service & timer installation guide
|
||||||
|
│ └── client_config.sample.json # Reference client configuration
|
||||||
|
└── win_client/
|
||||||
|
├── README.md # Windows service installation & configuration guide
|
||||||
|
└── client_config.sample.json # Reference client configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -208,15 +223,15 @@ LOGAR/
|
|||||||
|
|
||||||
1. **Install dependencies**:
|
1. **Install dependencies**:
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r compilation/requirements.txt
|
||||||
```
|
```
|
||||||
2. **Start the server** (generates `server_config.json` and keypair on first run):
|
2. **Start the server** (generates `server_config.json` and keypair on first run):
|
||||||
```bash
|
```bash
|
||||||
python Server.py
|
python src/Server.py
|
||||||
```
|
```
|
||||||
3. **Export a client configuration**:
|
3. **Export a client configuration**:
|
||||||
```bash
|
```bash
|
||||||
python Server.py --create-client-config --server-host <SERVER_IP> --server-port 9443 --client-out client_config.json
|
python src/Server.py --create-client-config --server-host <SERVER_IP> --server-port 9443 --client-out client_config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Windows Client Deployment
|
### 2. Windows Client Deployment
|
||||||
@@ -287,23 +302,23 @@ The pipeline test exercises invalid token rejection, encrypted socket streaming,
|
|||||||
|
|
||||||
1. **Start the server** in Shell 1 (creates `server_config.json` on first run):
|
1. **Start the server** in Shell 1 (creates `server_config.json` on first run):
|
||||||
```bash
|
```bash
|
||||||
python Server.py
|
python src/Server.py
|
||||||
```
|
```
|
||||||
2. **Export client configuration** in Shell 2 (required for testing):
|
2. **Export client configuration** in Shell 2 (required for testing):
|
||||||
```bash
|
```bash
|
||||||
python Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
|
python src/Server.py --create-client-config --server-host 127.0.0.1 --server-port 9443 --client-out client_config.json
|
||||||
```
|
```
|
||||||
3. **Execute the integration test** in Shell 2:
|
3. **Execute the integration test** in Shell 2:
|
||||||
```bash
|
```bash
|
||||||
python test_pipeline.py
|
python tests/test_pipeline.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Continuous Integration via Gitea Actions
|
## Continuous Integration via Gitea Actions
|
||||||
|
|
||||||
Continuous integration is automated via [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) and triggers automatically on **every push** and pull request:
|
Continuous integration is automated via [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) and triggers automatically on **every push** and pull request:
|
||||||
1. **Syntax Compilation**: Validates all Python scripts (`Server.py`, `Win_Client.py`, `Linux_Client.py`, `package_dist.py`, `upload_release.py`, `test_pipeline.py`, and test suites).
|
1. **Syntax Compilation**: Validates all Python scripts (`src/Server.py`, `src/Win_Client.py`, `src/Linux_Client.py`, `compilation/package_dist.py`, `compilation/upload_release.py`, `tests/test_pipeline.py`, and test suites).
|
||||||
2. **Component Unit Tests**: Discovers and runs all unit tests in `tests/` (`test_server.py`, `test_win_client.py`, `test_linux_client.py`).
|
2. **Component Unit Tests**: Discovers and runs all unit tests in `tests/` (`test_server.py`, `test_win_client.py`, `test_linux_client.py`).
|
||||||
3. **End-to-End Pipeline Verification**: Automatically spins up the LOGAR server hub, generates test configs, runs `test_pipeline.py` (testing socket authentication, 4-run rule persistence, Hermes API report, and client integrations), and shuts down the test instance.
|
3. **End-to-End Pipeline Verification**: Automatically spins up the LOGAR server hub, generates test configs, runs `tests/test_pipeline.py` (testing socket authentication, 4-run rule persistence, Hermes API report, and client integrations), and shuts down the test instance.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -316,8 +331,8 @@ Release builds are automated via two dedicated Gitea Actions workflows running c
|
|||||||
### Publishing a Release
|
### Publishing a Release
|
||||||
Whenever you want to release a new version with compiled standalone binaries:
|
Whenever you want to release a new version with compiled standalone binaries:
|
||||||
```bash
|
```bash
|
||||||
git tag v1.0.1
|
git tag v1.0.2
|
||||||
git push origin v1.0.1
|
git push origin v1.0.2
|
||||||
```
|
```
|
||||||
*(You can also trigger builds manually via the Gitea UI using the **Run workflow** button (`workflow_dispatch`) on either workflow).*
|
*(You can also trigger builds manually via the Gitea UI using the **Run workflow** button (`workflow_dispatch`) on either workflow).*
|
||||||
|
|
||||||
@@ -360,13 +375,13 @@ git push origin v1.0.1
|
|||||||
You can also compile and package binaries locally anytime:
|
You can also compile and package binaries locally anytime:
|
||||||
```bash
|
```bash
|
||||||
# Windows
|
# Windows
|
||||||
py -3.12 package_dist.py --target windows
|
py -3.12 compilation/package_dist.py --target windows
|
||||||
|
|
||||||
# Linux
|
# Linux
|
||||||
python3 package_dist.py --target linux
|
python3 compilation/package_dist.py --target linux
|
||||||
```
|
```
|
||||||
To upload local builds directly to Gitea:
|
To upload local builds directly to Gitea:
|
||||||
```powershell
|
```powershell
|
||||||
python upload_release.py --tag v1.0.1 --token <YOUR_GITEA_TOKEN>
|
python compilation/upload_release.py --tag v1.0.2 --token <YOUR_GITEA_TOKEN>
|
||||||
```
|
```
|
||||||
*(Environment variables `GITEA_TOKEN`, `GITEA_SERVER_URL`, `GITEA_REPOSITORY`, and `GITEA_REF_NAME` are also supported automatically).*
|
*(Environment variables `GITEA_TOKEN`, `GITEA_SERVER_URL`, `GITEA_REPOSITORY`, and `GITEA_REF_NAME` are also supported automatically).*
|
||||||
|
|||||||
+5
-2
@@ -1,7 +1,10 @@
|
|||||||
# LOGAR Release v1.0.1
|
# LOGAR Release v1.0.2
|
||||||
|
|
||||||
### Changes in this Release:
|
### Changes in this Release:
|
||||||
- **Dual Platform Gitea Release Automation**: Added dedicated Windows (`release-windows.yml`) and Linux (`release-linux.yml`) Gitea Actions to compile native platform binaries (`Win_Client.exe` and `Server.exe` on Windows; `Linux_Client.bin` and `Server.bin` on Linux).
|
- **Warning Persistence & Immediate Error Routing**: Restructured temporal verification on the central hub so the 4-run persistence rule across the 12-hour evaluation window strictly governs `WARNING` and `INFO` events to suppress transient blips. High-severity `ERROR`, `CRITICAL`, and `FATAL` events are now promoted to `VERIFIED` immediately on their first occurrence and reported to Hermes without waiting for consecutive runs.
|
||||||
|
- **Server Deployment Packages in `out/`**: Added comprehensive deployment guides and configuration templates for both Linux Server hub (systemd service) and Windows Server hub (NSSM service / Task Scheduler) under `out/linux_server` and `out/win_server`.
|
||||||
|
- **Refactored Repository Layout**: Reorganized codebase by moving runtime forwarders and server hub into `src/`, compilation/release packaging utilities into `compilation/`, and all unit and pipeline verification tests into `tests/`.
|
||||||
|
- **Dual Platform Gitea Release Automation**: Dedicated Windows (`release-windows.yml`) and Linux (`release-linux.yml`) Gitea Actions to compile native platform binaries (`Win_Client.exe` and `Server.exe` on Windows; `Linux_Client.bin` and `Server.bin` on Linux).
|
||||||
- **Dedicated SHA-256 Checksums**: Release assets now include dedicated checksum files matching `[win/linux]_[client/agent]_sha256sum` (`win_client_sha256sum`, `win_agent_sha256sum`, `win_server_sha256sum`, `linux_client_sha256sum`, `linux_agent_sha256sum`, `linux_server_sha256sum`).
|
- **Dedicated SHA-256 Checksums**: Release assets now include dedicated checksum files matching `[win/linux]_[client/agent]_sha256sum` (`win_client_sha256sum`, `win_agent_sha256sum`, `win_server_sha256sum`, `linux_client_sha256sum`, `linux_agent_sha256sum`, `linux_server_sha256sum`).
|
||||||
- **Removed Client Filter Logic**: Removed restrictive source-level noise filtering on edge forwarders. Clients now collect and stream all candidate events from `INFO` up to `ERROR` over the lookback window instead of discarding them at the source.
|
- **Removed Client Filter Logic**: Removed restrictive source-level noise filtering on edge forwarders. Clients now collect and stream all candidate events from `INFO` up to `ERROR` over the lookback window instead of discarding them at the source.
|
||||||
- **State Tracking & Deduplication**: Added persistent client state tracking (`client_state.json`) with journalctl cursors and Windows Event Log record numbers to guarantee that previously transmitted events are never resent.
|
- **State Tracking & Deduplication**: Added persistent client state tracking (`client_state.json`) with journalctl cursors and Windows Event Log record numbers to guarantee that previously transmitted events are never resent.
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import platform
|
|||||||
import subprocess
|
import subprocess
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
DIST_DIR = os.path.abspath("dist")
|
SRC_DIR = os.path.join(ROOT_DIR, "src")
|
||||||
BUILD_TEMP = os.path.abspath("build_temp")
|
DIST_DIR = os.path.join(ROOT_DIR, "dist")
|
||||||
|
BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
|
||||||
|
|
||||||
def clean_and_prep():
|
def clean_and_prep():
|
||||||
if os.path.exists(DIST_DIR):
|
if os.path.exists(DIST_DIR):
|
||||||
@@ -55,7 +56,7 @@ def build_linux_zipapp_fallback():
|
|||||||
# Linux Client zipapp
|
# Linux Client zipapp
|
||||||
app_dir = os.path.join(BUILD_TEMP, "linux_app")
|
app_dir = os.path.join(BUILD_TEMP, "linux_app")
|
||||||
os.makedirs(app_dir, exist_ok=True)
|
os.makedirs(app_dir, exist_ok=True)
|
||||||
shutil.copy(os.path.join(ROOT_DIR, "Linux_Client.py"), os.path.join(app_dir, "Linux_Client.py"))
|
shutil.copy(os.path.join(SRC_DIR, "Linux_Client.py"), os.path.join(app_dir, "Linux_Client.py"))
|
||||||
client_out = os.path.join(DIST_DIR, "Linux_Client.bin")
|
client_out = os.path.join(DIST_DIR, "Linux_Client.bin")
|
||||||
zipapp.create_archive(
|
zipapp.create_archive(
|
||||||
source=app_dir,
|
source=app_dir,
|
||||||
@@ -66,7 +67,7 @@ def build_linux_zipapp_fallback():
|
|||||||
# Server zipapp
|
# Server zipapp
|
||||||
srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
|
srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
|
||||||
os.makedirs(srv_dir, exist_ok=True)
|
os.makedirs(srv_dir, exist_ok=True)
|
||||||
shutil.copy(os.path.join(ROOT_DIR, "Server.py"), os.path.join(srv_dir, "Server.py"))
|
shutil.copy(os.path.join(SRC_DIR, "Server.py"), os.path.join(srv_dir, "Server.py"))
|
||||||
server_out = os.path.join(DIST_DIR, "Server.bin")
|
server_out = os.path.join(DIST_DIR, "Server.bin")
|
||||||
zipapp.create_archive(
|
zipapp.create_archive(
|
||||||
source=srv_dir,
|
source=srv_dir,
|
||||||
@@ -77,10 +78,10 @@ def build_linux_zipapp_fallback():
|
|||||||
|
|
||||||
def build_windows():
|
def build_windows():
|
||||||
print("[*] Compiling Windows standalone executables...")
|
print("[*] Compiling Windows standalone executables...")
|
||||||
win_client_script = os.path.join(ROOT_DIR, "Win_Client.py")
|
win_client_script = os.path.join(SRC_DIR, "Win_Client.py")
|
||||||
build_pyinstaller_binary(win_client_script, "Win_Client")
|
build_pyinstaller_binary(win_client_script, "Win_Client")
|
||||||
|
|
||||||
server_script = os.path.join(ROOT_DIR, "Server.py")
|
server_script = os.path.join(SRC_DIR, "Server.py")
|
||||||
build_pyinstaller_binary(server_script, "Server")
|
build_pyinstaller_binary(server_script, "Server")
|
||||||
|
|
||||||
client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
|
client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
|
||||||
@@ -111,10 +112,10 @@ def build_linux():
|
|||||||
is_linux_host = platform.system() == "Linux"
|
is_linux_host = platform.system() == "Linux"
|
||||||
|
|
||||||
if is_linux_host:
|
if is_linux_host:
|
||||||
linux_client_script = os.path.join(ROOT_DIR, "Linux_Client.py")
|
linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py")
|
||||||
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
|
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
|
||||||
|
|
||||||
server_script = os.path.join(ROOT_DIR, "Server.py")
|
server_script = os.path.join(SRC_DIR, "Server.py")
|
||||||
build_pyinstaller_binary(server_script, "Server.bin")
|
build_pyinstaller_binary(server_script, "Server.bin")
|
||||||
|
|
||||||
# Normalize extensions in case PyInstaller dropped .bin
|
# Normalize extensions in case PyInstaller dropped .bin
|
||||||
@@ -4,7 +4,8 @@ import json
|
|||||||
import argparse
|
import argparse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import mimetypes
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
import package_dist
|
import package_dist
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@@ -162,8 +163,8 @@ def main():
|
|||||||
if not notes and args.notes_file and os.path.exists(args.notes_file):
|
if not notes and args.notes_file and os.path.exists(args.notes_file):
|
||||||
with open(args.notes_file, "r", encoding="utf-8") as nf:
|
with open(args.notes_file, "r", encoding="utf-8") as nf:
|
||||||
notes = nf.read()
|
notes = nf.read()
|
||||||
elif not notes and os.path.exists("RELEASE_NOTES.md"):
|
elif not notes and os.path.exists(os.path.join(ROOT_DIR, "RELEASE_NOTES.md")):
|
||||||
with open("RELEASE_NOTES.md", "r", encoding="utf-8") as nf:
|
with open(os.path.join(ROOT_DIR, "RELEASE_NOTES.md"), "r", encoding="utf-8") as nf:
|
||||||
notes = nf.read()
|
notes = nf.read()
|
||||||
elif not notes:
|
elif not notes:
|
||||||
notes = (
|
notes = (
|
||||||
@@ -184,9 +185,9 @@ def main():
|
|||||||
print("[*] Assembling compiled binaries...")
|
print("[*] Assembling compiled binaries...")
|
||||||
package_dist.main()
|
package_dist.main()
|
||||||
|
|
||||||
dist_dir = os.path.abspath("dist")
|
dist_dir = os.path.join(ROOT_DIR, "dist")
|
||||||
if not os.path.exists(dist_dir) or not os.listdir(dist_dir):
|
if not os.path.exists(dist_dir) or not os.listdir(dist_dir):
|
||||||
print("[!] No binaries found in dist/. Run package_dist.py first.")
|
print(f"[!] No binaries found in {dist_dir}. Run package_dist.py first.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"[*] Connecting to Gitea: {args.url} (repo: {args.repo})...")
|
print(f"[*] Connecting to Gitea: {args.url} (repo: {args.repo})...")
|
||||||
@@ -22,7 +22,7 @@ Standalone compiled binary distribution for Linux edge servers running systemd.
|
|||||||
Run the following command on your central LOGAR server to export a client bundle tailored for your environment:
|
Run the following command on your central LOGAR server to export a client bundle tailored for your environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python Server.py --create-client-config --server-host <SERVER_IP_OR_DNS> --server-port 9443 --client-out client_config.json
|
python src/Server.py --create-client-config --server-host <SERVER_IP_OR_DNS> --server-port 9443 --client-out client_config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
- Replace `<SERVER_IP_OR_DNS>` with the reachable IP address or FQDN of your central LOGAR server hub.
|
- Replace `<SERVER_IP_OR_DNS>` with the reachable IP address or FQDN of your central LOGAR server hub.
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# LOGAR Linux Server Hub
|
||||||
|
|
||||||
|
Standalone compiled executable binary distribution for Linux server environments (`Server.bin`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`Server.bin` is a self-contained, pre-compiled Linux ELF executable that operates as the central coordination and log analysis hub of the LOGAR telemetry architecture.
|
||||||
|
|
||||||
|
### Key Architecture & Capabilities
|
||||||
|
- **Pre-compiled & Dependency-Free**: Ships as a standalone native Linux ELF binary (`Server.bin`). No Python runtime, pip dependencies, or GnuPG binaries are required on the host system.
|
||||||
|
- **Authenticated TCP Ingestion Socket (Port 9443)**: Accepts framed OpenPGP encrypted log batches streamed by edge forwarders (`Linux_Client.bin` and `Win_Client.exe`).
|
||||||
|
- **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 are evaluated against an episodic threshold, requiring persistence across at least 4 distinct client transmission cycles within a sliding 12-hour evaluation window before promotion from transient noise to `VERIFIED`.
|
||||||
|
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external scrapers, SIEM collectors, and alerting dashboards.
|
||||||
|
- **Pure-Python OpenPGP Cryptography**: Zero dependency on external `gpg` binaries. Automatically generates RSA-2048 encryption keys and SHA-256 fingerprints on first launch.
|
||||||
|
- **State Database**: Tracks anomaly lifecycles, run counters, and machine telemetry in a local SQLite state database (`logar_state.db`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initializing & Generating Server Configuration
|
||||||
|
|
||||||
|
### Step 1: Automatic First-Run Generation
|
||||||
|
When launched without an existing `server_config.json`, `Server.bin` automatically generates:
|
||||||
|
1. A fresh OpenPGP RSA-2048 encryption keypair (`private_key` and `public_key`).
|
||||||
|
2. A SHA-256 public encryption fingerprint (`server_fingerprint`).
|
||||||
|
3. A cryptographically random secret authentication token (`auth_token`).
|
||||||
|
4. Default network socket coordinates (TCP 9443, Hermes API 8443).
|
||||||
|
|
||||||
|
Run `Server.bin` once to initialize:
|
||||||
|
```bash
|
||||||
|
./Server.bin
|
||||||
|
```
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
[!] Config 'server_config.json' not found. Initializing first-run configuration...
|
||||||
|
[+] Successfully generated new server config and OpenPGP keypair.
|
||||||
|
[+] Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
|
||||||
|
[+] Saved to: server_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Configuration Fields Reference
|
||||||
|
The generated `server_config.json` contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_name": "LOGAR-Linux-Hub",
|
||||||
|
"tcp_host": "0.0.0.0",
|
||||||
|
"tcp_port": 9443,
|
||||||
|
"hermes_host": "0.0.0.0",
|
||||||
|
"hermes_port": 8443,
|
||||||
|
"auth_token": "a1b2c3d4e5f67890abcdef1234567890...",
|
||||||
|
"db_path": "logar_state.db",
|
||||||
|
"evaluation_window_hours": 12,
|
||||||
|
"min_persistence_runs": 4,
|
||||||
|
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
|
||||||
|
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
||||||
|
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `server_name` | `"LOGAR-Linux-Hub"` | Human-readable identifier for this hub instance |
|
||||||
|
| `tcp_host` | `"0.0.0.0"` | Network interface to bind for edge client TCP ingestion |
|
||||||
|
| `tcp_port` | `9443` | TCP port for incoming edge log batches |
|
||||||
|
| `hermes_host` | `"0.0.0.0"` | Network interface to bind for Hermes HTTP API |
|
||||||
|
| `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint |
|
||||||
|
| `auth_token` | *(auto-generated)* | Pre-shared secret required in edge client envelopes |
|
||||||
|
| `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database |
|
||||||
|
| `evaluation_window_hours` | `12` | Sliding temporal window for warning persistence |
|
||||||
|
| `min_persistence_runs` | `4` | Number of distinct runs required to promote warnings to `VERIFIED` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Generating Client Configuration Bundles
|
||||||
|
|
||||||
|
Edge forwarders (`Linux_Client.bin` and `Win_Client.exe`) require a minimal, anonymous configuration bundle containing socket coordinates, the authentication token, and the server's public key (without sensitive server names or private keys).
|
||||||
|
|
||||||
|
Run the following command on the server:
|
||||||
|
```bash
|
||||||
|
./Server.bin --create-client-config --server-host <SERVER_PUBLIC_OR_INTERNAL_IP> --server-port 9443 --client-out client_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `<SERVER_PUBLIC_OR_INTERNAL_IP>` with the reachable IP or FQDN of your LOGAR server.
|
||||||
|
- The output `client_config.json` can be distributed directly to Linux and Windows edge forwarder nodes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Running Interactively
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./Server.bin --config /path/to/server_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-Line Arguments
|
||||||
|
| Argument | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--config` | Path to server configuration JSON file (default: `server_config.json`) |
|
||||||
|
| `--create-client-config` | Exports an anonymous client configuration bundle and exits |
|
||||||
|
| `--server-host` | Hostname/IP to embed in the exported client configuration |
|
||||||
|
| `--server-port` | Port to embed in the exported client configuration (default: `9443`) |
|
||||||
|
| `--client-out` | Destination path for exported client configuration (default: `client_config.json`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Installing as a Systemd Service (Recommended)
|
||||||
|
|
||||||
|
Running `Server.bin` as a native systemd background service ensures continuous execution, automatic restart upon reboot or crash, and centralized log management via `journalctl`.
|
||||||
|
|
||||||
|
### Step 1: Create Deployment Directory and User
|
||||||
|
```bash
|
||||||
|
# Create dedicated system group and user
|
||||||
|
sudo useradd --system --no-create-home --shell /usr/sbin/nologin logar
|
||||||
|
|
||||||
|
# Prepare deployment folder
|
||||||
|
sudo mkdir -p /opt/logar-server
|
||||||
|
sudo cp Server.bin server_config.json /opt/logar-server/
|
||||||
|
sudo chmod +x /opt/logar-server/Server.bin
|
||||||
|
sudo chown -R logar:logar /opt/logar-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Systemd Service File
|
||||||
|
Create `/etc/systemd/system/logar-server.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=LOGAR Central Server Hub Service
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=logar
|
||||||
|
Group=logar
|
||||||
|
WorkingDirectory=/opt/logar-server
|
||||||
|
ExecStart=/opt/logar-server/Server.bin --config /opt/logar-server/server_config.json
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
LimitNOFILE=65536
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Enable and Start Service
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now logar-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Verify Status and Inspect Logs
|
||||||
|
```bash
|
||||||
|
# Check service status
|
||||||
|
sudo systemctl status logar-server.service
|
||||||
|
|
||||||
|
# Stream live server logs
|
||||||
|
sudo journalctl -u logar-server.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Hermes Reporting API & Integration
|
||||||
|
|
||||||
|
The server embeds a high-performance HTTP service on port `8443` providing real-time intelligence on promoted anomalies:
|
||||||
|
|
||||||
|
### Fetching Promoted Anomalies
|
||||||
|
```bash
|
||||||
|
curl -s http://127.0.0.1:8443/api/hermes/report | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Schema:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fingerprint": "prod-web-01.corp.internal:Out_Of_Memory",
|
||||||
|
"server": "prod-web-01.corp.internal",
|
||||||
|
"signature": "Out_Of_Memory",
|
||||||
|
"consecutive_runs": 4,
|
||||||
|
"first_seen": "2026-09-04T08:00:00Z",
|
||||||
|
"last_seen": "2026-09-04T14:30:00Z",
|
||||||
|
"status": "VERIFIED",
|
||||||
|
"verified": true,
|
||||||
|
"os_type": "linux",
|
||||||
|
"sample_message": "kernel: Out of memory: Kill process 1824"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Firewall Configuration
|
||||||
|
|
||||||
|
Ensure the following inbound ports are open on your host firewall:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# UFW (Ubuntu / Debian)
|
||||||
|
sudo ufw allow 9443/tcp comment "LOGAR TCP Log Ingestion"
|
||||||
|
sudo ufw allow 8443/tcp comment "LOGAR Hermes Reporting API"
|
||||||
|
sudo ufw reload
|
||||||
|
|
||||||
|
# Firewalld (RHEL / CentOS / Rocky / Alma)
|
||||||
|
sudo firewall-cmd --permanent --add-port=9443/tcp
|
||||||
|
sudo firewall-cmd --permanent --add-port=8443/tcp
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
```
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"server_name": "LOGAR-Linux-Hub",
|
||||||
|
"tcp_host": "0.0.0.0",
|
||||||
|
"tcp_port": 9443,
|
||||||
|
"hermes_host": "0.0.0.0",
|
||||||
|
"hermes_port": 8443,
|
||||||
|
"auth_token": "replace_with_secure_random_hex_token",
|
||||||
|
"db_path": "logar_state.db",
|
||||||
|
"evaluation_window_hours": 12,
|
||||||
|
"min_persistence_runs": 4,
|
||||||
|
"server_fingerprint": "AUTO_GENERATED_ON_FIRST_RUN",
|
||||||
|
"public_key": "AUTO_GENERATED_ON_FIRST_RUN",
|
||||||
|
"private_key": "AUTO_GENERATED_ON_FIRST_RUN"
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ Standalone compiled executable distribution for Windows Server and workstation e
|
|||||||
Run the following command on your central LOGAR server to export a client bundle tailored for your environment:
|
Run the following command on your central LOGAR server to export a client bundle tailored for your environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python Server.py --create-client-config --server-host <SERVER_IP_OR_DNS> --server-port 9443 --client-out client_config.json
|
python src/Server.py --create-client-config --server-host <SERVER_IP_OR_DNS> --server-port 9443 --client-out client_config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
- Replace `<SERVER_IP_OR_DNS>` with the reachable IP address or FQDN of your central LOGAR server hub.
|
- Replace `<SERVER_IP_OR_DNS>` with the reachable IP address or FQDN of your central LOGAR server hub.
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# LOGAR Windows Server Hub
|
||||||
|
|
||||||
|
Standalone compiled executable distribution for Windows Server environments (`Server.exe`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`Server.exe` is a self-contained, pre-compiled native Windows PE executable that serves as the central log aggregation, temporal persistence analyzer, and reporting hub of the LOGAR infrastructure.
|
||||||
|
|
||||||
|
### Key Architecture & Capabilities
|
||||||
|
- **Pre-compiled & Dependency-Free**: Ships as a standalone Windows executable (`Server.exe`). No Python installation, pip packages, or GnuPG binaries are required on Windows Server.
|
||||||
|
- **Authenticated TCP Ingestion Socket (Port 9443)**: Ingests framed OpenPGP encrypted log batches streamed from edge forwarder nodes (`Win_Client.exe` and `Linux_Client.bin`).
|
||||||
|
- **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 are evaluated against an episodic threshold, requiring persistence across at least 4 distinct client transmission cycles within a rolling 12-hour evaluation window before promotion to `VERIFIED`.
|
||||||
|
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external dashboards, monitoring agents, and scrapers.
|
||||||
|
- **Pure-Python OpenPGP Cryptography**: Automatically generates RSA-2048 encryption keys and a SHA-256 fingerprint on first launch without external dependencies.
|
||||||
|
- **State Database**: Stores issue lifecycle records, run counters, and machine telemetry in a local SQLite database (`logar_state.db`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initializing & Generating Server Configuration
|
||||||
|
|
||||||
|
### Step 1: Automatic First-Run Generation
|
||||||
|
When launched without an existing `server_config.json`, `Server.exe` automatically initializes:
|
||||||
|
1. An OpenPGP RSA-2048 encryption keypair (`private_key` and `public_key`).
|
||||||
|
2. A SHA-256 public encryption fingerprint (`server_fingerprint`).
|
||||||
|
3. A cryptographically random secret authentication token (`auth_token`).
|
||||||
|
4. Default network socket coordinates (TCP 9443, Hermes API 8443).
|
||||||
|
|
||||||
|
Open PowerShell and run:
|
||||||
|
```powershell
|
||||||
|
.\Server.exe
|
||||||
|
```
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
[!] Config 'server_config.json' not found. Initializing first-run configuration...
|
||||||
|
[+] Successfully generated new server config and OpenPGP keypair.
|
||||||
|
[+] Server Encryption Fingerprint: 375388960531264EA0648EC0D2C4E4ABC6F22AC2
|
||||||
|
[+] Saved to: server_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Configuration Fields Reference
|
||||||
|
The generated `server_config.json` contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_name": "LOGAR-Windows-Hub",
|
||||||
|
"tcp_host": "0.0.0.0",
|
||||||
|
"tcp_port": 9443,
|
||||||
|
"hermes_host": "0.0.0.0",
|
||||||
|
"hermes_port": 8443,
|
||||||
|
"auth_token": "a1b2c3d4e5f67890abcdef1234567890...",
|
||||||
|
"db_path": "logar_state.db",
|
||||||
|
"evaluation_window_hours": 12,
|
||||||
|
"min_persistence_runs": 4,
|
||||||
|
"server_fingerprint": "375388960531264EA0648EC0D2C4E4ABC6F22AC2",
|
||||||
|
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
||||||
|
"private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `server_name` | `"LOGAR-Windows-Hub"` | Identifier for this hub instance |
|
||||||
|
| `tcp_host` | `"0.0.0.0"` | Network interface to bind for incoming client socket traffic |
|
||||||
|
| `tcp_port` | `9443` | TCP port for incoming edge log batches |
|
||||||
|
| `hermes_host` | `"0.0.0.0"` | Network interface to bind for Hermes HTTP API |
|
||||||
|
| `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint |
|
||||||
|
| `auth_token` | *(auto-generated)* | Pre-shared authentication secret required in client envelopes |
|
||||||
|
| `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database |
|
||||||
|
| `evaluation_window_hours` | `12` | Rolling evaluation window in hours for warning persistence |
|
||||||
|
| `min_persistence_runs` | `4` | Consecutive runs required to promote warning issues to `VERIFIED` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Generating Client Configuration Bundles
|
||||||
|
|
||||||
|
Edge forwarders (`Win_Client.exe` and `Linux_Client.bin`) require an anonymous client configuration bundle that includes the server socket target, authentication token, and encryption public key, without exposing sensitive server names or private keys.
|
||||||
|
|
||||||
|
Run the following command on the server:
|
||||||
|
```powershell
|
||||||
|
.\Server.exe --create-client-config --server-host <SERVER_IP_OR_FQDN> --server-port 9443 --client-out client_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `<SERVER_IP_OR_FQDN>` with the reachable IP or DNS name of your LOGAR server.
|
||||||
|
- Distribute `client_config.json` to client forwarder nodes along with `Win_Client.exe` or `Linux_Client.bin`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Running Interactively
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Server.exe --config C:\LOGAR-Server\server_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-Line Arguments
|
||||||
|
| Argument | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--config` | Path to server configuration JSON file (default: `server_config.json`) |
|
||||||
|
| `--create-client-config` | Exports an anonymous client configuration bundle and exits |
|
||||||
|
| `--server-host` | Hostname/IP to embed in the exported client configuration |
|
||||||
|
| `--server-port` | Port to embed in the exported client configuration (default: `9443`) |
|
||||||
|
| `--client-out` | Destination path for exported client configuration (default: `client_config.json`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Installing as a Continuous Windows Service
|
||||||
|
|
||||||
|
Because `Server.exe` acts as a continuous server hub (listening for TCP connections and HTTP API queries), it should run persistently in the background.
|
||||||
|
|
||||||
|
### Method A: Native Windows Service via NSSM (Recommended)
|
||||||
|
[NSSM (Non-Sucking Service Manager)](https://nssm.cc/) is the industry standard for wrapping standalone executables into formal Windows services managed by `services.msc`.
|
||||||
|
|
||||||
|
1. Place `Server.exe` and `server_config.json` in `C:\LOGAR-Server\`.
|
||||||
|
2. Open **Elevated PowerShell (Run as Administrator)**:
|
||||||
|
```powershell
|
||||||
|
# Create deployment folder
|
||||||
|
New-Item -ItemType Directory -Path "C:\LOGAR-Server" -Force
|
||||||
|
Copy-Item "Server.exe", "server_config.json" -Destination "C:\LOGAR-Server\"
|
||||||
|
|
||||||
|
# Install Windows Service via NSSM
|
||||||
|
nssm.exe install LOGAR_Server "C:\LOGAR-Server\Server.exe" "--config C:\LOGAR-Server\server_config.json"
|
||||||
|
nssm.exe set LOGAR_Server AppDirectory "C:\LOGAR-Server"
|
||||||
|
nssm.exe set LOGAR_Server Description "LOGAR Central Aggregation Hub Service"
|
||||||
|
nssm.exe set LOGAR_Server Start SERVICE_AUTO_START
|
||||||
|
nssm.exe set LOGAR_Server AppStdout "C:\LOGAR-Server\server_out.log"
|
||||||
|
nssm.exe set LOGAR_Server AppStderr "C:\LOGAR-Server\server_err.log"
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
nssm.exe start LOGAR_Server
|
||||||
|
```
|
||||||
|
3. Verify status in PowerShell:
|
||||||
|
```powershell
|
||||||
|
Get-Service -Name "LOGAR_Server"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method B: Windows Task Scheduler (Startup Daemon)
|
||||||
|
If third-party service wrappers are restricted by organizational policy, configure a Task Scheduler job triggered at boot under the `SYSTEM` account:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Action: Launch Server.exe
|
||||||
|
$Action = New-ScheduledTaskAction -Execute "C:\LOGAR-Server\Server.exe" `
|
||||||
|
-Argument "--config C:\LOGAR-Server\server_config.json" `
|
||||||
|
-WorkingDirectory "C:\LOGAR-Server"
|
||||||
|
|
||||||
|
# Trigger: At system startup
|
||||||
|
$Trigger = New-ScheduledTaskTrigger -AtStartup
|
||||||
|
|
||||||
|
# Settings: Restart on failure, no execution time limit
|
||||||
|
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
|
||||||
|
-DontStopIfGoingOnBatteries `
|
||||||
|
-StartWhenAvailable `
|
||||||
|
-RestartCount 3 `
|
||||||
|
-RestartInterval (New-TimeSpan -Minutes 1) `
|
||||||
|
-ExecutionTimeLimit ([TimeSpan]::Zero)
|
||||||
|
|
||||||
|
# Register task under SYSTEM with highest privileges
|
||||||
|
Register-ScheduledTask -TaskName "LOGAR_Server_Daemon" `
|
||||||
|
-Action $Action `
|
||||||
|
-Trigger $Trigger `
|
||||||
|
-Settings $Settings `
|
||||||
|
-User "NT AUTHORITY\SYSTEM" `
|
||||||
|
-RunLevel Highest `
|
||||||
|
-Description "LOGAR Central Hub Daemon"
|
||||||
|
|
||||||
|
# Start the task immediately
|
||||||
|
Start-ScheduledTask -TaskName "LOGAR_Server_Daemon"
|
||||||
|
Get-ScheduledTask -TaskName "LOGAR_Server_Daemon"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Hermes Reporting API & Health Checks
|
||||||
|
|
||||||
|
Test the embedded Hermes REST endpoint locally using PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$report = Invoke-RestMethod -Uri "http://127.0.0.1:8443/api/hermes/report" -Method GET
|
||||||
|
$report | Format-Table fingerprint, status, consecutive_runs, first_seen, last_seen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Format:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fingerprint": "win-dc-01.corp.internal:DiskCorruptionDetected",
|
||||||
|
"server": "win-dc-01.corp.internal",
|
||||||
|
"signature": "DiskCorruptionDetected",
|
||||||
|
"consecutive_runs": 4,
|
||||||
|
"first_seen": "2026-09-04T08:15:00Z",
|
||||||
|
"last_seen": "2026-09-04T15:00:00Z",
|
||||||
|
"status": "VERIFIED",
|
||||||
|
"verified": true,
|
||||||
|
"os_type": "windows",
|
||||||
|
"sample_message": "An error was detected on device \\Device\\Harddisk0\\DR0 during a paging operation."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Windows Defender Firewall Configuration
|
||||||
|
|
||||||
|
Open the necessary inbound firewall ports to allow incoming edge forwarder socket streams and HTTP API queries:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Allow TCP 9443 for edge log forwarding
|
||||||
|
New-NetFirewallRule -DisplayName "LOGAR TCP Log Ingestion" `
|
||||||
|
-Direction Inbound `
|
||||||
|
-LocalPort 9443 `
|
||||||
|
-Protocol TCP `
|
||||||
|
-Action Allow
|
||||||
|
|
||||||
|
# Allow TCP 8443 for Hermes Reporting REST API
|
||||||
|
New-NetFirewallRule -DisplayName "LOGAR Hermes Reporting API" `
|
||||||
|
-Direction Inbound `
|
||||||
|
-LocalPort 8443 `
|
||||||
|
-Protocol TCP `
|
||||||
|
-Action Allow
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Uninstallation & Removal
|
||||||
|
|
||||||
|
To remove the server service:
|
||||||
|
```powershell
|
||||||
|
# If installed via NSSM:
|
||||||
|
nssm.exe stop LOGAR_Server
|
||||||
|
nssm.exe remove LOGAR_Server confirm
|
||||||
|
|
||||||
|
# If installed via Task Scheduler:
|
||||||
|
Unregister-ScheduledTask -TaskName "LOGAR_Server_Daemon" -Confirm:$false
|
||||||
|
|
||||||
|
# Clean files
|
||||||
|
Remove-Item -Recurse -Force "C:\LOGAR-Server"
|
||||||
|
```
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"server_name": "LOGAR-Windows-Hub",
|
||||||
|
"tcp_host": "0.0.0.0",
|
||||||
|
"tcp_port": 9443,
|
||||||
|
"hermes_host": "0.0.0.0",
|
||||||
|
"hermes_port": 8443,
|
||||||
|
"auth_token": "replace_with_secure_random_hex_token",
|
||||||
|
"db_path": "logar_state.db",
|
||||||
|
"evaluation_window_hours": 12,
|
||||||
|
"min_persistence_runs": 4,
|
||||||
|
"server_fingerprint": "AUTO_GENERATED_ON_FIRST_RUN",
|
||||||
|
"public_key": "AUTO_GENERATED_ON_FIRST_RUN",
|
||||||
|
"private_key": "AUTO_GENERATED_ON_FIRST_RUN"
|
||||||
|
}
|
||||||
@@ -183,6 +183,9 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
|
|||||||
if severity in ["DEBUG", "TRACE"]:
|
if severity in ["DEBUG", "TRACE"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Errors are always passed immediately; the 4-run rule only concerns warnings
|
||||||
|
is_error = severity in ["ERROR", "CRITICAL", "FATAL"]
|
||||||
|
|
||||||
signature = log.get("signature", "unknown")
|
signature = log.get("signature", "unknown")
|
||||||
server = log.get("server", client_server)
|
server = log.get("server", client_server)
|
||||||
message = log.get("message", "")
|
message = log.get("message", "")
|
||||||
@@ -207,7 +210,7 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
|
|||||||
# Window elapsed: reset to new cycle
|
# Window elapsed: reset to new cycle
|
||||||
new_runs = 1
|
new_runs = 1
|
||||||
new_first_seen = now_iso
|
new_first_seen = now_iso
|
||||||
new_status = "TRANSIENT"
|
new_status = "VERIFIED" if is_error else "TRANSIENT"
|
||||||
else:
|
else:
|
||||||
# Same run guard: only increment count once per distinct run batch
|
# Same run guard: only increment count once per distinct run batch
|
||||||
if last_run_id != run_id:
|
if last_run_id != run_id:
|
||||||
@@ -215,8 +218,8 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
|
|||||||
else:
|
else:
|
||||||
new_runs = run_count
|
new_runs = run_count
|
||||||
new_first_seen = first_seen_str
|
new_first_seen = first_seen_str
|
||||||
# 4-run rule enforcement
|
# 4-run rule applies to warnings; errors are always passed immediately as VERIFIED
|
||||||
new_status = "VERIFIED" if new_runs >= min_runs else "TRANSIENT"
|
new_status = "VERIFIED" if (is_error or new_runs >= min_runs) else "TRANSIENT"
|
||||||
|
|
||||||
if new_status == "VERIFIED" and current_status != "VERIFIED":
|
if new_status == "VERIFIED" and current_status != "VERIFIED":
|
||||||
promoted_to_verified += 1
|
promoted_to_verified += 1
|
||||||
@@ -227,7 +230,9 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
|
|||||||
WHERE fingerprint = ?
|
WHERE fingerprint = ?
|
||||||
""", (new_runs, now_iso, new_first_seen, new_status, run_id, message, severity, fp))
|
""", (new_runs, now_iso, new_first_seen, new_status, run_id, message, severity, fp))
|
||||||
else:
|
else:
|
||||||
initial_status = "VERIFIED" if 1 >= min_runs else "TRANSIENT"
|
initial_status = "VERIFIED" if (is_error or 1 >= min_runs) else "TRANSIENT"
|
||||||
|
if initial_status == "VERIFIED":
|
||||||
|
promoted_to_verified += 1
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO active_issues
|
INSERT INTO active_issues
|
||||||
(fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status, last_run_id)
|
(fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status, last_run_id)
|
||||||
@@ -328,8 +333,8 @@ def get_hermes_report():
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status
|
SELECT fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status
|
||||||
FROM active_issues
|
FROM active_issues
|
||||||
WHERE status = 'VERIFIED' AND run_count >= ?
|
WHERE status = 'VERIFIED'
|
||||||
""", (min_runs,))
|
""")
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -452,7 +457,7 @@ def main():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(f" LOGAR Server Hub: {config['server_name']}")
|
print(f" LOGAR Server Hub: {config['server_name']}")
|
||||||
print(f" Encryption Fingerprint: {config['server_fingerprint']}")
|
print(f" Encryption Fingerprint: {config['server_fingerprint']}")
|
||||||
print(f" Evaluation Window: {config['evaluation_window_hours']} hours | Rule: {config['min_persistence_runs']}+ consecutive runs")
|
print(f" Evaluation Window: {config['evaluation_window_hours']} hours | 4-Run Rule: Warnings | Immediate Pass: Errors")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -8,6 +8,7 @@ import warnings
|
|||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
|
|
||||||
import Linux_Client
|
import Linux_Client
|
||||||
import pgpy
|
import pgpy
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import urllib.request
|
|||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
# Ensure repository root and src/ directory are in sys.path
|
||||||
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
SRC_DIR = os.path.join(ROOT_DIR, "src")
|
||||||
|
sys.path.insert(0, ROOT_DIR)
|
||||||
|
sys.path.insert(0, SRC_DIR)
|
||||||
|
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
import pgpy
|
import pgpy
|
||||||
|
|
||||||
@@ -20,13 +26,16 @@ HERMES_PORT = 8443
|
|||||||
|
|
||||||
def run_tests():
|
def run_tests():
|
||||||
print("=== [1] Verifying server_config.json & client_config.json ===")
|
print("=== [1] Verifying server_config.json & client_config.json ===")
|
||||||
assert os.path.exists("server_config.json"), "server_config.json must exist"
|
server_cfg_path = "server_config.json" if os.path.exists("server_config.json") else os.path.join(ROOT_DIR, "server_config.json")
|
||||||
assert os.path.exists("client_config.json"), "client_config.json must exist"
|
client_cfg_path = "client_config.json" if os.path.exists("client_config.json") else os.path.join(ROOT_DIR, "client_config.json")
|
||||||
|
|
||||||
|
assert os.path.exists(server_cfg_path), f"{server_cfg_path} must exist"
|
||||||
|
assert os.path.exists(client_cfg_path), f"{client_cfg_path} must exist"
|
||||||
|
|
||||||
with open("client_config.json", "r", encoding="utf-8") as f:
|
with open(client_cfg_path, "r", encoding="utf-8") as f:
|
||||||
client_conf = json.load(f)
|
client_conf = json.load(f)
|
||||||
|
|
||||||
with open("server_config.json", "r", encoding="utf-8") as f:
|
with open(server_cfg_path, "r", encoding="utf-8") as f:
|
||||||
server_conf = json.load(f)
|
server_conf = json.load(f)
|
||||||
|
|
||||||
assert "server_name" not in client_conf, "client_config.json must NOT contain server_name"
|
assert "server_name" not in client_conf, "client_config.json must NOT contain server_name"
|
||||||
@@ -71,20 +80,25 @@ def run_tests():
|
|||||||
assert bad_resp.get("status") == "error", f"Expected error, got: {bad_resp}"
|
assert bad_resp.get("status") == "error", f"Expected error, got: {bad_resp}"
|
||||||
print(f"[OK] Bad auth rejected correctly: {bad_resp['message']}")
|
print(f"[OK] Bad auth rejected correctly: {bad_resp['message']}")
|
||||||
|
|
||||||
test_signature = "TestServiceCrash"
|
test_signature = "TestServiceDegraded"
|
||||||
candidate_log = [{
|
candidate_log = [{
|
||||||
"server": "test-edge-node",
|
"server": "test-edge-node",
|
||||||
"os_type": "linux",
|
"os_type": "linux",
|
||||||
"signature": test_signature,
|
"signature": test_signature,
|
||||||
"severity": "ERROR",
|
"severity": "WARNING",
|
||||||
"message": "Out of memory killer triggered"
|
"message": "Resource usage high warning"
|
||||||
}]
|
}]
|
||||||
|
|
||||||
print("\n=== [3] Testing Temporal Persistence & 4-Run Rule ===")
|
print("\n=== [3] Testing Temporal Persistence & 4-Run Rule for Warnings ===")
|
||||||
for run_num in range(1, 5):
|
for run_num in range(1, 5):
|
||||||
resp = send_socket_batch(candidate_log)
|
resp = send_socket_batch(candidate_log)
|
||||||
assert resp.get("status") == "success", f"Run {run_num} failed: {resp}"
|
assert resp.get("status") == "success", f"Run {run_num} failed: {resp}"
|
||||||
print(f"[Run {run_num}/4] Ingested successfully. Promoted to verified: {resp.get('promoted_verified')}")
|
promoted = resp.get("promoted_verified", 0)
|
||||||
|
print(f"[Run {run_num}/4] Ingested successfully. Promoted to verified: {promoted}")
|
||||||
|
if run_num < 4:
|
||||||
|
assert promoted == 0, f"Expected 0 promoted on run {run_num} for warning, got {promoted}"
|
||||||
|
else:
|
||||||
|
assert promoted == 1, f"Expected 1 promoted on run 4 for warning, got {promoted}"
|
||||||
|
|
||||||
# Inspect SQLite database directly
|
# Inspect SQLite database directly
|
||||||
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
|
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
|
||||||
@@ -98,7 +112,33 @@ def run_tests():
|
|||||||
print(f"[DB Verification] Issue '{test_signature}' -> run_count: {run_count}, status: {status}")
|
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 run_count >= 4, f"Expected run_count >= 4, got {run_count}"
|
||||||
assert status == "VERIFIED", f"Expected status 'VERIFIED', got {status}"
|
assert status == "VERIFIED", f"Expected status 'VERIFIED', got {status}"
|
||||||
print("[OK] 4-Run Rule verified: Transient issue promoted to VERIFIED anomaly!")
|
print("[OK] 4-Run Rule verified: Warning promoted to VERIFIED anomaly on 4th run!")
|
||||||
|
|
||||||
|
print("\n=== [3b] Testing Immediate Pass for Errors ===")
|
||||||
|
error_signature = "TestServiceCrashImmediate"
|
||||||
|
error_log = [{
|
||||||
|
"server": "test-edge-node",
|
||||||
|
"os_type": "linux",
|
||||||
|
"signature": error_signature,
|
||||||
|
"severity": "ERROR",
|
||||||
|
"message": "Fatal process crash occurred"
|
||||||
|
}]
|
||||||
|
err_resp = send_socket_batch(error_log)
|
||||||
|
assert err_resp.get("status") == "success", f"Error run failed: {err_resp}"
|
||||||
|
print(f"[Run 1/1] Error ingested successfully. Promoted to verified: {err_resp.get('promoted_verified')}")
|
||||||
|
assert err_resp.get("promoted_verified") == 1, f"Expected error to be promoted to verified immediately on run 1, got {err_resp.get('promoted_verified')}"
|
||||||
|
|
||||||
|
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", (error_signature,))
|
||||||
|
err_row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
assert err_row is not None, "Error issue not found in SQLite"
|
||||||
|
err_run_count, err_status = err_row
|
||||||
|
print(f"[DB Verification] Issue '{error_signature}' -> run_count: {err_run_count}, status: {err_status}")
|
||||||
|
assert err_run_count == 1, f"Expected run_count == 1, got {err_run_count}"
|
||||||
|
assert err_status == "VERIFIED", f"Expected status 'VERIFIED', got {err_status}"
|
||||||
|
print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!")
|
||||||
|
|
||||||
print("\n=== [4] Testing Hermes Reporting Endpoint (/api/hermes/report) ===")
|
print("\n=== [4] Testing Hermes Reporting Endpoint (/api/hermes/report) ===")
|
||||||
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report")
|
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report")
|
||||||
@@ -107,15 +147,21 @@ def run_tests():
|
|||||||
hermes_data = json.loads(response.read().decode("utf-8"))
|
hermes_data = json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
print(f"[Hermes API] Returned {len(hermes_data)} verified anomalies:")
|
print(f"[Hermes API] Returned {len(hermes_data)} verified anomalies:")
|
||||||
found_issue = False
|
found_warning = False
|
||||||
|
found_error = False
|
||||||
for issue in hermes_data:
|
for issue in hermes_data:
|
||||||
print(f" - Fingerprint: {issue['fingerprint']} | Consecutive Runs: {issue['consecutive_runs']} | Status: {issue['status']}")
|
print(f" - Fingerprint: {issue['fingerprint']} | Consecutive Runs: {issue['consecutive_runs']} | Status: {issue['status']}")
|
||||||
if issue["signature"] == test_signature:
|
if issue["signature"] == test_signature:
|
||||||
found_issue = True
|
found_warning = True
|
||||||
assert issue["verified"] is True
|
assert issue["verified"] is True
|
||||||
assert issue["consecutive_runs"] >= 4
|
assert issue["consecutive_runs"] >= 4
|
||||||
|
if issue["signature"] == error_signature:
|
||||||
|
found_error = True
|
||||||
|
assert issue["verified"] is True
|
||||||
|
assert issue["consecutive_runs"] == 1
|
||||||
|
|
||||||
assert found_issue, f"Test issue {test_signature} should be in Hermes report"
|
assert found_warning, f"Warning issue {test_signature} should be in Hermes report"
|
||||||
|
assert found_error, f"Error issue {error_signature} should be in Hermes report"
|
||||||
print("[OK] Hermes reporting validated!")
|
print("[OK] Hermes reporting validated!")
|
||||||
|
|
||||||
print("\n=== [5] Testing Windows Client Script Integration ===")
|
print("\n=== [5] Testing Windows Client Script Integration ===")
|
||||||
+50
-15
@@ -11,8 +11,9 @@ from datetime import datetime, timezone, timedelta
|
|||||||
|
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
# Ensure parent directory is in path to import Server
|
# Ensure parent directory and src directory are in path to import Server
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
|
|
||||||
import Server
|
import Server
|
||||||
import pgpy
|
import pgpy
|
||||||
@@ -78,9 +79,9 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
Server.init_db(self.test_db)
|
Server.init_db(self.test_db)
|
||||||
log_entry = {
|
log_entry = {
|
||||||
"server": "app-worker-01.corp.local",
|
"server": "app-worker-01.corp.local",
|
||||||
"signature": "PostgresConnTimeout",
|
"signature": "PostgresConnWarning",
|
||||||
"severity": "ERROR",
|
"severity": "WARNING",
|
||||||
"message": "Connection to database pool timed out after 30s",
|
"message": "Connection to database pool near capacity: 85%",
|
||||||
"os_type": "linux"
|
"os_type": "linux"
|
||||||
}
|
}
|
||||||
payload = {
|
payload = {
|
||||||
@@ -88,7 +89,7 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
"logs": [log_entry]
|
"logs": [log_entry]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Runs 1 to 3: should remain TRANSIENT
|
# Runs 1 to 3: WARNING should remain TRANSIENT
|
||||||
for run_idx in range(1, 4):
|
for run_idx in range(1, 4):
|
||||||
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=4)
|
||||||
self.assertEqual(res["status"], "success")
|
self.assertEqual(res["status"], "success")
|
||||||
@@ -96,24 +97,53 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
|
|
||||||
conn = sqlite3.connect(self.test_db)
|
conn = sqlite3.connect(self.test_db)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnTimeout",))
|
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
self.assertEqual(row[0], 3)
|
self.assertEqual(row[0], 3)
|
||||||
self.assertEqual(row[1], "TRANSIENT")
|
self.assertEqual(row[1], "TRANSIENT")
|
||||||
|
|
||||||
# Run 4: promotes to VERIFIED!
|
# Run 4: promotes WARNING to VERIFIED!
|
||||||
res4 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
|
res4 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
|
||||||
self.assertEqual(res4["promoted_verified"], 1)
|
self.assertEqual(res4["promoted_verified"], 1)
|
||||||
|
|
||||||
conn = sqlite3.connect(self.test_db)
|
conn = sqlite3.connect(self.test_db)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnTimeout",))
|
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
self.assertEqual(row[0], 4)
|
self.assertEqual(row[0], 4)
|
||||||
self.assertEqual(row[1], "VERIFIED")
|
self.assertEqual(row[1], "VERIFIED")
|
||||||
|
|
||||||
|
def test_error_immediate_pass(self):
|
||||||
|
Server.init_db(self.test_db)
|
||||||
|
log_entry = {
|
||||||
|
"server": "app-worker-01.corp.local",
|
||||||
|
"signature": "KernelPanicCritical",
|
||||||
|
"severity": "ERROR",
|
||||||
|
"message": "Kernel panic - not syncing: Fatal hardware error",
|
||||||
|
"os_type": "linux"
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"server": "app-worker-01.corp.local",
|
||||||
|
"logs": [log_entry]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run 1: ERROR must immediately promote to VERIFIED
|
||||||
|
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"], 1)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(self.test_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT run_count, status, severity FROM active_issues WHERE signature = ?", ("KernelPanicCritical",))
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
self.assertIsNotNone(row)
|
||||||
|
self.assertEqual(row[0], 1)
|
||||||
|
self.assertEqual(row[1], "VERIFIED")
|
||||||
|
self.assertEqual(row[2], "ERROR")
|
||||||
|
|
||||||
def test_server_severity_filtering(self):
|
def test_server_severity_filtering(self):
|
||||||
Server.init_db(self.test_db)
|
Server.init_db(self.test_db)
|
||||||
payload = {
|
payload = {
|
||||||
@@ -131,15 +161,20 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
|
|
||||||
conn = sqlite3.connect(self.test_db)
|
conn = sqlite3.connect(self.test_db)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT signature FROM active_issues ORDER BY signature")
|
c.execute("SELECT signature, status FROM active_issues ORDER BY signature")
|
||||||
sigs = [r[0] for r in c.fetchall()]
|
rows = dict(c.fetchall())
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
self.assertIn("SigInfo", sigs)
|
self.assertIn("SigInfo", rows)
|
||||||
self.assertIn("SigWarn", sigs)
|
self.assertIn("SigWarn", rows)
|
||||||
self.assertIn("SigErr", sigs)
|
self.assertIn("SigErr", rows)
|
||||||
self.assertNotIn("SigDebug", sigs)
|
self.assertNotIn("SigDebug", rows)
|
||||||
self.assertNotIn("SigTrace", sigs)
|
self.assertNotIn("SigTrace", rows)
|
||||||
|
|
||||||
|
# SigErr is immediately VERIFIED; SigWarn and SigInfo are TRANSIENT on run 1
|
||||||
|
self.assertEqual(rows["SigErr"], "VERIFIED")
|
||||||
|
self.assertEqual(rows["SigWarn"], "TRANSIENT")
|
||||||
|
self.assertEqual(rows["SigInfo"], "TRANSIENT")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import warnings
|
|||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
|
|
||||||
import Win_Client
|
import Win_Client
|
||||||
import pgpy
|
import pgpy
|
||||||
|
|||||||
Reference in New Issue
Block a user