Refactor Server.py into modular domain classes (models, auth, storage, queue, licensing, PKI, UI)
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m14s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m14s
This commit is contained in:
+473
-3455
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
LOGAR - Server Authentication & Access Control Managers
|
||||
Provides modular classes for Session Management, Rate Limiting, Password Hashing,
|
||||
Two-Factor Authentication (TOTP / RFC 6238), and Hermes API Tokens.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import base64
|
||||
import struct
|
||||
import secrets
|
||||
import hashlib
|
||||
import hmac
|
||||
import sqlite3
|
||||
import urllib.parse
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
from fastapi import Request, HTTPException
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""Manages cryptographic session secrets and HMAC-signed session tokens."""
|
||||
|
||||
SESSION_COOKIE_NAME = "logar_server_session"
|
||||
_CACHED_SESSION_SECRETS: Dict[str, str] = {}
|
||||
|
||||
@classmethod
|
||||
def get_session_secret(cls, db_path: Optional[str] = None, fallback_db: str = "logar_state.db") -> str:
|
||||
"""Retrieves or persists a cryptographically secure session secret in the database."""
|
||||
env_sec = os.environ.get("LOGAR_SESSION_SECRET")
|
||||
if env_sec:
|
||||
return env_sec
|
||||
target_db = db_path or fallback_db
|
||||
abs_db = os.path.abspath(target_db)
|
||||
if abs_db in cls._CACHED_SESSION_SECRETS:
|
||||
return cls._CACHED_SESSION_SECRETS[abs_db]
|
||||
try:
|
||||
conn = sqlite3.connect(target_db, timeout=5.0)
|
||||
c = conn.cursor()
|
||||
c.execute("CREATE TABLE IF NOT EXISTS server_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
||||
c.execute("SELECT value FROM server_meta WHERE key = 'session_secret'")
|
||||
row = c.fetchone()
|
||||
if row and row[0]:
|
||||
cls._CACHED_SESSION_SECRETS[abs_db] = row[0]
|
||||
conn.close()
|
||||
return row[0]
|
||||
new_secret = secrets.token_hex(32)
|
||||
c.execute("INSERT OR REPLACE INTO server_meta (key, value) VALUES ('session_secret', ?)", (new_secret,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
cls._CACHED_SESSION_SECRETS[abs_db] = new_secret
|
||||
return new_secret
|
||||
except Exception:
|
||||
fallback = secrets.token_hex(32)
|
||||
cls._CACHED_SESSION_SECRETS[abs_db] = fallback
|
||||
return fallback
|
||||
|
||||
@classmethod
|
||||
def generate_session_token(cls, username: str, db_path: Optional[str] = None, fallback_db: str = "logar_state.db") -> str:
|
||||
"""Generates an HMAC-signed session cookie string."""
|
||||
ts = int(datetime.now(timezone.utc).timestamp())
|
||||
nonce = secrets.token_hex(8)
|
||||
payload = f"{username}:{ts}:{nonce}"
|
||||
secret = cls.get_session_secret(db_path=db_path, fallback_db=fallback_db)
|
||||
sig = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
return f"{payload}:{sig}"
|
||||
|
||||
@classmethod
|
||||
def verify_session_token(
|
||||
cls,
|
||||
token: Optional[str],
|
||||
max_age_hours: int = 24,
|
||||
db_path: Optional[str] = None,
|
||||
fallback_db: str = "logar_state.db"
|
||||
) -> Optional[str]:
|
||||
"""Verifies session token authenticity and age."""
|
||||
if not token or ":" not in token:
|
||||
return None
|
||||
parts = token.split(":")
|
||||
if len(parts) != 4:
|
||||
return None
|
||||
username, ts_str, nonce, sig = parts
|
||||
payload = f"{username}:{ts_str}:{nonce}"
|
||||
secret = cls.get_session_secret(db_path=db_path, fallback_db=fallback_db)
|
||||
expected_sig = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(sig, expected_sig):
|
||||
return None
|
||||
try:
|
||||
ts = int(ts_str)
|
||||
if datetime.now(timezone.utc).timestamp() - ts > max_age_hours * 3600:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return username
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""Sliding-window IP rate limiter with persistent SQLite tracking and memory fallback."""
|
||||
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self._login_failures: Dict[str, List[float]] = {}
|
||||
self._enroll_failures: Dict[str, List[float]] = {}
|
||||
|
||||
@staticmethod
|
||||
def get_client_ip(request: Optional[Request] = None) -> str:
|
||||
"""Extracts client IP address respecting reverse-proxy headers."""
|
||||
if not request:
|
||||
return "127.0.0.1"
|
||||
forwarded = request.headers.get("x-forwarded-for")
|
||||
if forwarded:
|
||||
return forwarded.split(",")[0].strip()
|
||||
real_ip = request.headers.get("x-real-ip")
|
||||
if real_ip:
|
||||
return real_ip.strip()
|
||||
if request.client:
|
||||
return request.client.host
|
||||
return "127.0.0.1"
|
||||
|
||||
def check_ip_rate_limit(
|
||||
self,
|
||||
ip: str,
|
||||
store: Any = None,
|
||||
max_attempts: int = 10,
|
||||
window_seconds: int = 60,
|
||||
scope: str = "login",
|
||||
db_path: Optional[str] = None,
|
||||
fallback_db: str = "logar_state.db"
|
||||
) -> bool:
|
||||
"""Checks whether the client IP has exceeded failed attempts within the sliding window."""
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
cutoff = now - window_seconds
|
||||
target_db = db_path or fallback_db
|
||||
try:
|
||||
conn = sqlite3.connect(target_db, timeout=5.0)
|
||||
c = conn.cursor()
|
||||
c.execute("CREATE TABLE IF NOT EXISTS rate_limits (ip TEXT NOT NULL, scope TEXT NOT NULL, attempt_time REAL NOT NULL)")
|
||||
c.execute("DELETE FROM rate_limits WHERE attempt_time < ?", (cutoff,))
|
||||
c.execute("SELECT COUNT(*) FROM rate_limits WHERE ip = ? AND scope = ? AND attempt_time >= ?", (ip, scope, cutoff))
|
||||
cnt = c.fetchone()[0]
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return cnt < max_attempts
|
||||
except Exception:
|
||||
with self._lock:
|
||||
effective_store = store if isinstance(store, dict) else (self._login_failures if scope == "login" else self._enroll_failures)
|
||||
history = effective_store.get(ip, [])
|
||||
history = [ts for ts in history if now - ts < window_seconds]
|
||||
effective_store[ip] = history
|
||||
return len(history) < max_attempts
|
||||
|
||||
def record_failed_ip_attempt(
|
||||
self,
|
||||
ip: str,
|
||||
store: Any = None,
|
||||
scope: str = "login",
|
||||
db_path: Optional[str] = None,
|
||||
fallback_db: str = "logar_state.db"
|
||||
):
|
||||
"""Records a persistent failed attempt timestamp for the client IP and scope."""
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
target_db = db_path or fallback_db
|
||||
try:
|
||||
conn = sqlite3.connect(target_db, timeout=5.0)
|
||||
c = conn.cursor()
|
||||
c.execute("CREATE TABLE IF NOT EXISTS rate_limits (ip TEXT NOT NULL, scope TEXT NOT NULL, attempt_time REAL NOT NULL)")
|
||||
c.execute("INSERT INTO rate_limits (ip, scope, attempt_time) VALUES (?, ?, ?)", (ip, scope, now))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
with self._lock:
|
||||
effective_store = store if isinstance(store, dict) else (self._login_failures if scope == "login" else self._enroll_failures)
|
||||
if ip not in effective_store:
|
||||
effective_store[ip] = []
|
||||
effective_store[ip].append(now)
|
||||
|
||||
def reset_failed_ip_attempts(
|
||||
self,
|
||||
ip: str,
|
||||
store: Any = None,
|
||||
scope: str = "login",
|
||||
db_path: Optional[str] = None,
|
||||
fallback_db: str = "logar_state.db"
|
||||
):
|
||||
"""Resets persistent failed attempt records for client IP upon successful authentication."""
|
||||
target_db = db_path or fallback_db
|
||||
try:
|
||||
conn = sqlite3.connect(target_db, timeout=5.0)
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM rate_limits WHERE ip = ? AND scope = ?", (ip, scope))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
with self._lock:
|
||||
effective_store = store if isinstance(store, dict) else (self._login_failures if scope == "login" else self._enroll_failures)
|
||||
if ip in effective_store:
|
||||
del effective_store[ip]
|
||||
|
||||
|
||||
class PasswordManager:
|
||||
"""PBKDF2-HMAC-SHA256 password hashing and validation."""
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str, salt: Optional[str] = None) -> Tuple[str, str]:
|
||||
"""Hashes a password using PBKDF2-HMAC-SHA256 with 100,000 iterations."""
|
||||
if not salt:
|
||||
salt = secrets.token_hex(16)
|
||||
pwd_hash = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 100000).hex()
|
||||
return pwd_hash, salt
|
||||
|
||||
@staticmethod
|
||||
def verify_password(password: str, salt: str, password_hash: str) -> bool:
|
||||
"""Verifies a password against the stored salt and PBKDF2 hash."""
|
||||
test_hash, _ = PasswordManager.hash_password(password, salt=salt)
|
||||
return hmac.compare_digest(test_hash, password_hash)
|
||||
|
||||
@staticmethod
|
||||
def verify_user_credentials(username: str, password: str, db_path: str = "logar_state.db") -> bool:
|
||||
"""Checks user credentials against SQLite users table."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT password_hash, salt FROM users WHERE username = ?", (username,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return False
|
||||
return PasswordManager.verify_password(password, row[1], row[0])
|
||||
|
||||
@staticmethod
|
||||
def change_user_password(username: str, new_password: str, db_path: str = "logar_state.db") -> bool:
|
||||
"""Updates a user's password."""
|
||||
pwd_hash, salt = PasswordManager.hash_password(new_password)
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("UPDATE users SET password_hash = ?, salt = ? WHERE username = ?", (pwd_hash, salt, username))
|
||||
changed = c.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return changed
|
||||
|
||||
|
||||
class TOTPManager:
|
||||
"""RFC 6238 Two-Factor Authentication (TOTP) generator and validator."""
|
||||
|
||||
@staticmethod
|
||||
def generate_totp_secret(length: int = 20) -> str:
|
||||
"""Generates a cryptographically random Base32-encoded TOTP secret key."""
|
||||
return base64.b32encode(secrets.token_bytes(length)).decode("utf-8").replace("=", "")
|
||||
|
||||
@staticmethod
|
||||
def get_totp_token(secret: str, intervals_no: Optional[int] = None) -> str:
|
||||
"""Calculates a 6-digit TOTP code for the given counter interval according to RFC 6238."""
|
||||
if intervals_no is None:
|
||||
intervals_no = int(time.time()) // 30
|
||||
key = base64.b32decode(secret + "=" * ((8 - len(secret) % 8) % 8), casefold=True)
|
||||
msg = struct.pack(">Q", intervals_no)
|
||||
h = hmac.new(key, msg, hashlib.sha1).digest()
|
||||
o = h[19] & 15
|
||||
h_int = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
|
||||
return f"{h_int:06d}"
|
||||
|
||||
@staticmethod
|
||||
def verify_totp_code(secret: str, code: str, window: int = 1) -> bool:
|
||||
"""Verifies a 6-digit TOTP code with standard drift window (+/- 30s)."""
|
||||
if not secret or not code:
|
||||
return False
|
||||
code = str(code).strip().replace(" ", "")
|
||||
if len(code) != 6 or not code.isdigit():
|
||||
return False
|
||||
now = int(time.time())
|
||||
current_step = now // 30
|
||||
for offset in range(-window, window + 1):
|
||||
if hmac.compare_digest(TOTPManager.get_totp_token(secret, current_step + offset), code):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_totp_uri(username: str, secret: str, issuer: str = "LOGAR Server Hub") -> str:
|
||||
"""Generates standard otpauth URI for QR code generation."""
|
||||
user_enc = urllib.parse.quote(username)
|
||||
issuer_enc = urllib.parse.quote(issuer)
|
||||
return f"otpauth://totp/{issuer_enc}:{user_enc}?secret={secret}&issuer={issuer_enc}&algorithm=SHA1&digits=6&period=30"
|
||||
|
||||
@staticmethod
|
||||
def get_user_totp_info(username: str, db_path: str = "logar_state.db") -> Dict[str, Any]:
|
||||
"""Retrieves TOTP configuration for a user from the database."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("SELECT totp_secret, totp_enabled FROM users WHERE username = ?", (username,))
|
||||
row = c.fetchone()
|
||||
if row:
|
||||
return {"secret": row[0] or "", "enabled": bool(row[1])}
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
conn.close()
|
||||
return {"secret": "", "enabled": False}
|
||||
|
||||
@staticmethod
|
||||
def enable_user_totp(username: str, secret: str, db_path: str = "logar_state.db") -> bool:
|
||||
"""Enables and activates TOTP for a user in the database."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("UPDATE users SET totp_secret = ?, totp_enabled = 1 WHERE username = ?", (secret, username))
|
||||
changed = c.rowcount > 0
|
||||
conn.commit()
|
||||
return changed
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def disable_user_totp(username: str, db_path: str = "logar_state.db") -> bool:
|
||||
"""Disables TOTP for a user in the database."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("UPDATE users SET totp_secret = NULL, totp_enabled = 0 WHERE username = ?", (username,))
|
||||
changed = c.rowcount > 0
|
||||
conn.commit()
|
||||
return changed
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class HermesTokenManager:
|
||||
"""Manages Bearer API tokens for Hermes AI anomaly integration."""
|
||||
|
||||
@staticmethod
|
||||
def create_hermes_token(name: str, token: Optional[str] = None, db_path: str = "logar_state.db") -> Dict[str, Any]:
|
||||
"""Creates or saves a named Hermes API token."""
|
||||
if not token:
|
||||
token = f"hermes_{secrets.token_hex(20)}"
|
||||
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO hermes_tokens (token, name, created_at, status) VALUES (?, ?, ?, 'active')",
|
||||
(token, name, now_iso)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"token": token, "name": name, "created_at": now_iso, "status": "active"}
|
||||
|
||||
@staticmethod
|
||||
def list_hermes_tokens(db_path: str = "logar_state.db") -> List[Dict[str, Any]]:
|
||||
"""Lists all registered Hermes access tokens."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT token, name, created_at, last_used, status FROM hermes_tokens ORDER BY created_at DESC")
|
||||
rows = [
|
||||
{"token": r[0], "name": r[1], "created_at": r[2], "last_used": r[3] or "Never", "status": r[4]}
|
||||
for r in c.fetchall()
|
||||
]
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
@staticmethod
|
||||
def revoke_hermes_token(token: str, db_path: str = "logar_state.db") -> bool:
|
||||
"""Revokes an active Hermes token."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("UPDATE hermes_tokens SET status = 'revoked' WHERE token = ?", (token,))
|
||||
changed = c.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def delete_hermes_token(token: str, db_path: str = "logar_state.db") -> bool:
|
||||
"""Deletes a Hermes token."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM hermes_tokens WHERE token = ?", (token,))
|
||||
changed = c.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def verify_hermes_token(token: str, db_path: str = "logar_state.db") -> bool:
|
||||
"""Verifies a Hermes token and updates last_used timestamp."""
|
||||
if not token:
|
||||
return False
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT token FROM hermes_tokens WHERE token = ? AND status = 'active'", (token,))
|
||||
row = c.fetchone()
|
||||
if row:
|
||||
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
c.execute("UPDATE hermes_tokens SET last_used = ? WHERE token = ?", (now_iso, token))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return row is not None
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
LOGAR - Ingestion Queue Manager
|
||||
Provides a high-throughput thread-safe ring buffer and batch processor for incoming telemetry.
|
||||
Eliminates disk write contention by decoupling TCP frame ingress from physical database writes.
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
|
||||
|
||||
class IngestQueueManager:
|
||||
"""
|
||||
High-throughput in-memory buffering and batch processor for incoming telemetry and log anomalies.
|
||||
Eliminates SQLite / SQL disk write contention by decoupling TCP frame ingress from physical DB writes.
|
||||
"""
|
||||
def __init__(self, flush_interval: float = 1.0, batch_size: int = 100, max_queue: int = 50000):
|
||||
self.flush_interval = flush_interval
|
||||
self.batch_size = batch_size
|
||||
self.max_queue = max_queue
|
||||
self._lock = threading.Lock()
|
||||
self._buffer: List[Dict[str, Any]] = []
|
||||
self._total_ingested = 0
|
||||
self._total_flushed = 0
|
||||
self._dropped_count = 0
|
||||
self._last_flush_time = "Never"
|
||||
self._avg_flush_latency_ms = 0.0
|
||||
self._flush_count = 0
|
||||
|
||||
def push(self, payload: Dict[str, Any]) -> bool:
|
||||
"""Pushes a log payload into the ring buffer."""
|
||||
with self._lock:
|
||||
if len(self._buffer) >= self.max_queue:
|
||||
self._dropped_count += 1
|
||||
return False
|
||||
self._buffer.append(payload)
|
||||
self._total_ingested += len(payload.get("logs", []))
|
||||
return True
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Returns live statistics about queue occupancy, throughput, and flush latency."""
|
||||
with self._lock:
|
||||
return {
|
||||
"queue_size": len(self._buffer),
|
||||
"total_ingested": self._total_ingested,
|
||||
"total_flushed": self._total_flushed,
|
||||
"dropped_count": self._dropped_count,
|
||||
"last_flush_time": self._last_flush_time,
|
||||
"avg_flush_latency_ms": round(self._avg_flush_latency_ms, 2),
|
||||
"flush_count": self._flush_count,
|
||||
"flush_interval_seconds": self.flush_interval,
|
||||
"batch_size_threshold": self.batch_size
|
||||
}
|
||||
|
||||
def flush(
|
||||
self,
|
||||
db_path: Optional[str] = None,
|
||||
window_hours: int = 12,
|
||||
min_runs: int = 1,
|
||||
process_callback: Optional[Callable[..., Dict[str, Any]]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Flushes all queued items to persistent storage."""
|
||||
with self._lock:
|
||||
if not self._buffer:
|
||||
return {"status": "noop", "flushed_runs": 0, "processed_logs": 0}
|
||||
items_to_process = list(self._buffer)
|
||||
self._buffer.clear()
|
||||
|
||||
start_time = time.time()
|
||||
target_db = db_path or "logar_state.db"
|
||||
|
||||
total_processed = 0
|
||||
promoted_total = 0
|
||||
|
||||
try:
|
||||
for payload in items_to_process:
|
||||
if process_callback:
|
||||
res = process_callback(
|
||||
payload=payload,
|
||||
db_path=target_db,
|
||||
window_hours=window_hours,
|
||||
min_runs=min_runs
|
||||
)
|
||||
else:
|
||||
# Fallback if imported directly
|
||||
from src import Server
|
||||
res = Server.process_ingested_logs(
|
||||
payload=payload,
|
||||
db_path=target_db,
|
||||
window_hours=window_hours,
|
||||
min_runs=min_runs
|
||||
)
|
||||
total_processed += res.get("processed", 0)
|
||||
promoted_total += res.get("promoted_verified", 0)
|
||||
except Exception as e:
|
||||
print(f"[!] Error during batch flush to DB: {e}")
|
||||
|
||||
elapsed_ms = (time.time() - start_time) * 1000.0
|
||||
with self._lock:
|
||||
self._total_flushed += total_processed
|
||||
self._flush_count += 1
|
||||
self._avg_flush_latency_ms = (
|
||||
(self._avg_flush_latency_ms * (self._flush_count - 1) + elapsed_ms) / self._flush_count
|
||||
)
|
||||
self._last_flush_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"flushed_runs": len(items_to_process),
|
||||
"processed_logs": total_processed,
|
||||
"promoted_verified": promoted_total,
|
||||
"flush_latency_ms": round(elapsed_ms, 2)
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
LOGAR - Licensing & Sub-CA Introspection Manager
|
||||
Provides modular classes for PKI X.509 extension parsing, capability gating,
|
||||
Subordinate CA seat tracking, and asynchronous license reporting.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import socket
|
||||
import sqlite3
|
||||
import threading
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
|
||||
try:
|
||||
from src import server_enrollment as enrollment
|
||||
except ImportError:
|
||||
import server_enrollment as enrollment
|
||||
|
||||
|
||||
class LicenseManager:
|
||||
"""Manages PKI licensing introspection, feature unlocking, and upstream seat syncing."""
|
||||
|
||||
DEFAULT_DB_FILE = "logar_state.db"
|
||||
|
||||
@staticmethod
|
||||
def export_license_request_lic(config: Dict[str, Any]) -> Tuple[Dict[str, Any], str]:
|
||||
"""Generates CA CSR and packages into exportable .lic dictionary and JSON string."""
|
||||
cert_dir = config.get("cert_dir", "certs")
|
||||
lic_server = config.get("license_server_url", "https://logar.eibl.tech")
|
||||
hostname = socket.gethostname()
|
||||
seats = config.get("max_seats", 10)
|
||||
fp = config.get("server_fingerprint", "")
|
||||
return enrollment.create_license_request_lic(
|
||||
cert_dir=cert_dir,
|
||||
license_server=lic_server,
|
||||
server_fingerprint=fp,
|
||||
hostname=hostname,
|
||||
requested_seats=seats
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_active_licensed_features(db_path: Optional[str] = None, server_state: Optional[Dict[str, Any]] = None) -> List[str]:
|
||||
"""
|
||||
Returns the list of all active enterprise capability identifiers
|
||||
authorized either via X.509 certificate extensions or the persistent database license config.
|
||||
"""
|
||||
features = set()
|
||||
state = server_state or {}
|
||||
|
||||
# 1. Check in-memory / active Subordinate CA certificate
|
||||
ca_pem = state.get("ca_cert_pem")
|
||||
if not ca_pem:
|
||||
cert_dir = state.get("config", {}).get("cert_dir", "certs")
|
||||
ca_file = os.path.join(cert_dir, "ca.crt")
|
||||
if os.path.exists(ca_file):
|
||||
try:
|
||||
with open(ca_file, "r", encoding="utf-8") as f:
|
||||
ca_pem = f.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if ca_pem:
|
||||
try:
|
||||
info = enrollment.extract_license_info_from_cert(ca_pem)
|
||||
for feat in info.get("features", []):
|
||||
if feat:
|
||||
features.add(str(feat).strip())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Check license_config table in database
|
||||
target_db = db_path or state.get("config", {}).get("db_path", LicenseManager.DEFAULT_DB_FILE)
|
||||
if target_db and os.path.exists(target_db):
|
||||
try:
|
||||
conn = sqlite3.connect(target_db, timeout=3.0)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT features FROM license_config WHERE id = 1")
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if row and row[0]:
|
||||
feat_raw = row[0]
|
||||
if isinstance(feat_raw, str):
|
||||
if feat_raw.startswith("["):
|
||||
feat_list = json.loads(feat_raw)
|
||||
else:
|
||||
feat_list = [f.strip() for f in feat_raw.split(",") if f.strip()]
|
||||
for f_item in feat_list:
|
||||
if f_item:
|
||||
features.add(str(f_item).strip())
|
||||
elif isinstance(feat_raw, list):
|
||||
for f_item in feat_raw:
|
||||
if f_item:
|
||||
features.add(str(f_item).strip())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return sorted(list(features))
|
||||
|
||||
@staticmethod
|
||||
def is_feature_licensed(feature: str, db_path: Optional[str] = None, server_state: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""Checks whether a specific enterprise capability (e.g. 'enterprise_db') is authorized."""
|
||||
if not feature:
|
||||
return False
|
||||
return feature in LicenseManager.get_active_licensed_features(db_path=db_path, server_state=server_state)
|
||||
|
||||
@staticmethod
|
||||
def get_license_summary(server_state: Optional[Dict[str, Any]] = None, db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Inspects certs/ca.crt, database quotas, and returns comprehensive licensing status."""
|
||||
state = server_state or {}
|
||||
config = state.get("config", {})
|
||||
cert_dir = config.get("cert_dir", "certs")
|
||||
ca_path = os.path.join(cert_dir, "ca.crt")
|
||||
target_db = db_path or config.get("db_path", LicenseManager.DEFAULT_DB_FILE)
|
||||
|
||||
max_seats = config.get("max_seats", 0)
|
||||
license_status_db = "unlicensed"
|
||||
try:
|
||||
conn = sqlite3.connect(target_db)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT max_seats, license_status FROM license_config WHERE id = 1")
|
||||
row = c.fetchone()
|
||||
if row:
|
||||
if row[0] is not None:
|
||||
max_seats = row[0]
|
||||
if len(row) > 1 and row[1]:
|
||||
license_status_db = row[1]
|
||||
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'active' AND cert_fingerprint != 'LOCAL-HOST-BUILTIN'")
|
||||
active_seats = c.fetchone()[0]
|
||||
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'revoked' AND cert_fingerprint != 'LOCAL-HOST-BUILTIN'")
|
||||
revoked_seats = c.fetchone()[0]
|
||||
conn.close()
|
||||
except Exception:
|
||||
active_seats = 0
|
||||
revoked_seats = 0
|
||||
|
||||
csr_path = os.path.join(cert_dir, "ca.csr")
|
||||
csr_exists = os.path.exists(csr_path)
|
||||
|
||||
if os.path.exists(ca_path):
|
||||
try:
|
||||
with open(ca_path, "r", encoding="utf-8") as f:
|
||||
ca_pem = f.read()
|
||||
info = enrollment.extract_license_info_from_cert(ca_pem)
|
||||
if info.get("seats"):
|
||||
max_seats = info["seats"]
|
||||
except Exception:
|
||||
info = {"valid": False}
|
||||
else:
|
||||
info = {"valid": False, "is_self_signed": False, "is_expired": False, "days_remaining": 0}
|
||||
|
||||
license_id = info.get("license_id") or "UNLICENSED"
|
||||
issuer_url = info.get("issuer_url") or config.get("license_server_url", "https://logar.eibl.tech")
|
||||
|
||||
if info.get("is_expired"):
|
||||
status_label = "EXPIRED"
|
||||
elif info.get("seats") and not info.get("is_self_signed"):
|
||||
status_label = "ACTIVE"
|
||||
elif max_seats > 0 and license_status_db == "active":
|
||||
status_label = "ACTIVE"
|
||||
elif info.get("is_self_signed"):
|
||||
status_label = "SELF-SIGNED" if max_seats > 0 else "UNLICENSED"
|
||||
else:
|
||||
status_label = "UNLICENSED" if max_seats <= 0 else "ACTIVE"
|
||||
|
||||
pct = int((active_seats / max_seats * 100)) if max_seats > 0 else 0
|
||||
return {
|
||||
"status": status_label,
|
||||
"active_seats": active_seats,
|
||||
"active_seats_taken": active_seats,
|
||||
"revoked_seats": revoked_seats,
|
||||
"max_seats": max_seats,
|
||||
"available_seats": max(0, max_seats - active_seats),
|
||||
"percentage": min(100, pct),
|
||||
"license_id": license_id,
|
||||
"issuer_url": issuer_url,
|
||||
"issuer_cn": info.get("issuer_cn", "N/A"),
|
||||
"subject_cn": info.get("subject_cn", "N/A"),
|
||||
"is_self_signed": info.get("is_self_signed", False),
|
||||
"not_valid_before": info.get("not_valid_before", "N/A"),
|
||||
"not_valid_after": info.get("not_valid_after", "N/A"),
|
||||
"days_remaining": info.get("days_remaining", 0),
|
||||
"fingerprint": info.get("fingerprint", "N/A"),
|
||||
"csr_exists": csr_exists,
|
||||
"server_fingerprint": config.get("server_fingerprint", "N/A"),
|
||||
"features": LicenseManager.get_active_licensed_features(db_path=target_db, server_state=state)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def report_seat_quota_to_authority_sync(server_state: Optional[Dict[str, Any]] = None):
|
||||
"""Background helper to push active seat count to upstream authority without blocking."""
|
||||
try:
|
||||
state = server_state or {}
|
||||
config = state.get("config", {})
|
||||
cert_dir = config.get("cert_dir", "certs")
|
||||
db_path = config.get("db_path", LicenseManager.DEFAULT_DB_FILE)
|
||||
lic_server = config.get("license_server_url", "https://logar.eibl.tech")
|
||||
if not os.path.exists(os.path.join(cert_dir, "ca.crt")) or not os.path.exists(os.path.join(cert_dir, "ca.key")):
|
||||
return
|
||||
|
||||
active_seats = 0
|
||||
if os.path.exists(db_path):
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
row = conn.execute("SELECT COUNT(*) FROM clients WHERE status = 'active' AND cert_fingerprint != 'LOCAL-HOST-BUILTIN'").fetchone()
|
||||
if row:
|
||||
active_seats = row[0]
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
enrollment.renew_ca_cert_mtls(
|
||||
cert_dir=cert_dir,
|
||||
license_server_url=lic_server,
|
||||
seats_taken=active_seats,
|
||||
active_seats=active_seats,
|
||||
hostname=config.get("server_name", ""),
|
||||
server_fingerprint=config.get("server_fingerprint", "")
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def trigger_async_seat_report(server_state: Optional[Dict[str, Any]] = None):
|
||||
"""Fires a non-blocking background thread to sync seat usage with upstream authority."""
|
||||
threading.Thread(target=LicenseManager.report_seat_quota_to_authority_sync, args=(server_state,), daemon=True).start()
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
LOGAR - Dynamic PKI & mTLS Context Manager
|
||||
Provides modular classes for TLS 1.3 socket contexts, in-flight certificate rotation,
|
||||
and automated Sub-CA renewal.
|
||||
"""
|
||||
|
||||
import os
|
||||
import ssl
|
||||
import sqlite3
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
try:
|
||||
from src import server_enrollment as enrollment
|
||||
except ImportError:
|
||||
import server_enrollment as enrollment
|
||||
|
||||
|
||||
class PKIManager:
|
||||
"""Manages server TLS certificates, mTLS SSL contexts, and hot certificate reloading."""
|
||||
|
||||
@staticmethod
|
||||
def init_mtls_server_context(cert_dir: str = "certs") -> ssl.SSLContext:
|
||||
"""Initializes TLS 1.3 server SSLContext with client certificate requirement (mTLS)."""
|
||||
ca_file = os.path.join(cert_dir, "ca.crt")
|
||||
srv_cert = os.path.join(cert_dir, "server.crt")
|
||||
srv_key = os.path.join(cert_dir, "server.key")
|
||||
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
||||
ctx.load_verify_locations(cafile=ca_file)
|
||||
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
|
||||
if os.path.exists(upstream_file):
|
||||
try:
|
||||
ctx.load_verify_locations(cafile=upstream_file)
|
||||
except Exception:
|
||||
pass
|
||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
return ctx
|
||||
|
||||
@staticmethod
|
||||
def reload_mtls_context(ssl_ctx: ssl.SSLContext, cert_dir: str = "certs"):
|
||||
"""
|
||||
Dynamically reloads server certificate chain and Root CA in an active SSLContext.
|
||||
Allows in-flight TLS certificate rotation without dropping the listening socket.
|
||||
"""
|
||||
ca_file = os.path.join(cert_dir, "ca.crt")
|
||||
srv_cert = os.path.join(cert_dir, "server.crt")
|
||||
srv_key = os.path.join(cert_dir, "server.key")
|
||||
|
||||
ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
||||
ssl_ctx.load_verify_locations(cafile=ca_file)
|
||||
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
|
||||
if os.path.exists(upstream_file):
|
||||
try:
|
||||
ssl_ctx.load_verify_locations(cafile=upstream_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def check_and_rotate_server_certs(
|
||||
cert_dir: str = "certs",
|
||||
hostnames: Optional[List[str]] = None,
|
||||
threshold_days: int = 30,
|
||||
server_state: Optional[Dict[str, Any]] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if Root/Subordinate CA or server TLS certificate are expiring within threshold_days.
|
||||
If a licensed Subordinate CA is expiring, automatically connects over mTLS to logar.eibl.tech
|
||||
to request an in-flight renewal, then reloads the active SSLContext without socket interruption.
|
||||
Returns True if renewed/reloaded, False otherwise.
|
||||
"""
|
||||
state = server_state or {}
|
||||
config = state.get("config", {})
|
||||
license_server = config.get("license_server_url", "https://logar.eibl.tech")
|
||||
ca_cert_path = os.path.join(cert_dir, "ca.crt")
|
||||
|
||||
# 1. Check if CA certificate is expiring and attempt upstream mTLS renewal
|
||||
if os.path.exists(ca_cert_path):
|
||||
try:
|
||||
with open(ca_cert_path, "r", encoding="utf-8") as f:
|
||||
ca_pem = f.read()
|
||||
if enrollment.is_cert_expiring_soon(ca_pem, threshold_days=threshold_days):
|
||||
info = enrollment.extract_license_info_from_cert(ca_pem)
|
||||
if not info.get("is_self_signed", True) or info.get("license_id"):
|
||||
print("[*] Subordinate CA expiring soon. Initiating automated mTLS renewal with logar.eibl.tech...")
|
||||
success, msg, new_pem = enrollment.renew_ca_cert_mtls(
|
||||
cert_dir=cert_dir,
|
||||
license_server_url=license_server
|
||||
)
|
||||
if success and new_pem:
|
||||
print(f"[+] Automated mTLS renewal successful: {msg}")
|
||||
new_info = enrollment.extract_license_info_from_cert(new_pem)
|
||||
if new_info.get("seats"):
|
||||
config["max_seats"] = new_info["seats"]
|
||||
db_path = config.get("db_path", "logar_state.db")
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("UPDATE license_config SET max_seats = ? WHERE id = 1", (new_info["seats"],))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
ca_cert, ca_key, ca_pem, ca_key_pem = enrollment.generate_ca_if_needed(cert_dir=cert_dir, force_renew=False)
|
||||
state["ca_cert"] = ca_cert
|
||||
state["ca_key"] = ca_key
|
||||
state["ca_cert_pem"] = ca_pem
|
||||
enrollment.generate_server_cert_if_needed(ca_cert, ca_key, hostnames=hostnames, cert_dir=cert_dir, force_renew=True)
|
||||
ssl_ctx = state.get("ssl_ctx")
|
||||
if ssl_ctx is not None:
|
||||
try:
|
||||
PKIManager.reload_mtls_context(ssl_ctx, cert_dir=cert_dir)
|
||||
print("[+] In-flight mTLS SSLContext successfully reloaded with renewed CA.")
|
||||
except Exception as e:
|
||||
print(f"[!] Failed to reload in-flight SSLContext: {e}")
|
||||
return True
|
||||
else:
|
||||
print(f"[!] Automated mTLS renewal attempt failed: {msg}")
|
||||
except Exception as e:
|
||||
print(f"[!] Error evaluating CA certificate for mTLS renewal: {e}")
|
||||
|
||||
# 2. Local self-healing / regeneration check
|
||||
ca_renewed, srv_renewed = enrollment.check_and_renew_hub_pki(
|
||||
cert_dir=cert_dir,
|
||||
hostnames=hostnames,
|
||||
threshold_days=threshold_days
|
||||
)
|
||||
if ca_renewed or srv_renewed:
|
||||
print(f"[!] Server Hub PKI certificates renewed (CA renewed: {ca_renewed}, Server cert renewed: {srv_renewed}).")
|
||||
ca_cert, ca_key, ca_pem, ca_key_pem = enrollment.generate_ca_if_needed(cert_dir=cert_dir, force_renew=False)
|
||||
state["ca_cert"] = ca_cert
|
||||
state["ca_key"] = ca_key
|
||||
state["ca_cert_pem"] = ca_pem
|
||||
|
||||
ssl_ctx = state.get("ssl_ctx")
|
||||
if ssl_ctx is not None:
|
||||
try:
|
||||
PKIManager.reload_mtls_context(ssl_ctx, cert_dir=cert_dir)
|
||||
print("[+] In-flight mTLS SSLContext successfully reloaded with updated certificates.")
|
||||
except Exception as e:
|
||||
print(f"[!] Failed to reload in-flight SSLContext: {e}")
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
LOGAR - Server Pydantic Models
|
||||
Provides structured request and response models for the LOGAR Server Hub API.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ClientEnrollRequest(BaseModel):
|
||||
client_id: str
|
||||
hostname: str
|
||||
os: str
|
||||
enrollment_secret: str
|
||||
|
||||
|
||||
class ClientActionRequest(BaseModel):
|
||||
client_id: str
|
||||
|
||||
|
||||
class LicenseImportRequest(BaseModel):
|
||||
ca_cert: str
|
||||
upstream_ca: Optional[str] = None
|
||||
|
||||
|
||||
class HermesTokenCreateRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class HermesTokenRevokeRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
|
||||
class ClientProfileExportRequest(BaseModel):
|
||||
server_host: Optional[str] = None
|
||||
server_port: Optional[int] = None
|
||||
client_id: Optional[str] = None
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class DbConnectionTestRequest(BaseModel):
|
||||
host: str = "localhost"
|
||||
port: int = 5432
|
||||
database: str = "logar"
|
||||
user: str = "postgres"
|
||||
password: str = ""
|
||||
sslmode: str = "prefer"
|
||||
connect_timeout: int = 5
|
||||
pool_size: int = 10
|
||||
|
||||
|
||||
class DbSwitchBackendRequest(BaseModel):
|
||||
db_type: str = "sqlite" # "sqlite" | "postgres"
|
||||
host: Optional[str] = "localhost"
|
||||
port: Optional[int] = 5432
|
||||
database: Optional[str] = "logar"
|
||||
user: Optional[str] = "postgres"
|
||||
password: Optional[str] = ""
|
||||
sslmode: Optional[str] = "prefer"
|
||||
connect_timeout: Optional[int] = 5
|
||||
pool_size: Optional[int] = 10
|
||||
db_path: Optional[str] = None
|
||||
|
||||
|
||||
class DbFullConfigRequest(BaseModel):
|
||||
storage_engine: str = "sqlite" # "sqlite" | "postgres"
|
||||
cache_mode: str = "direct" # "direct" | "buffered"
|
||||
flush_interval: float = 2.0
|
||||
batch_size: int = 100
|
||||
max_queue: int = 50000
|
||||
encrypt_at_rest: bool = False
|
||||
pg_config: Optional[DbConnectionTestRequest] = None
|
||||
@@ -0,0 +1,638 @@
|
||||
"""
|
||||
LOGAR - Multi-Backend Storage Engine & Data-at-Rest Crypto
|
||||
Provides modular classes for Database schema management, SQLite WAL persistence,
|
||||
PostgreSQL enterprise backend, and OpenPGP / AES-256-GCM data encryption at rest.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import sqlite3
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Any, List, Tuple, Callable
|
||||
import pgpy
|
||||
from pgpy.constants import (
|
||||
PubKeyAlgorithm,
|
||||
KeyFlags,
|
||||
HashAlgorithm,
|
||||
SymmetricKeyAlgorithm,
|
||||
CompressionAlgorithm
|
||||
)
|
||||
|
||||
from src.auth_manager import PasswordManager
|
||||
|
||||
|
||||
class StorageCryptoManager:
|
||||
"""Manages OpenPGP keypairs and AES-256-GCM zero-knowledge data-at-rest encryption."""
|
||||
|
||||
@staticmethod
|
||||
def generate_server_keypair(server_name: str) -> Tuple[str, str, str]:
|
||||
"""Generates an OpenPGP RSA 2048 key with encryption capability."""
|
||||
key = pgpy.PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 2048)
|
||||
uid = pgpy.PGPUID.new(server_name)
|
||||
key.add_uid(
|
||||
uid,
|
||||
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
|
||||
hashes=[HashAlgorithm.SHA256, HashAlgorithm.SHA512],
|
||||
ciphers=[SymmetricKeyAlgorithm.AES256, SymmetricKeyAlgorithm.AES128],
|
||||
compression=[
|
||||
CompressionAlgorithm.ZIP,
|
||||
CompressionAlgorithm.ZLIB,
|
||||
CompressionAlgorithm.BZ2,
|
||||
CompressionAlgorithm.Uncompressed
|
||||
]
|
||||
)
|
||||
private_key_armored = str(key)
|
||||
public_key_armored = str(key.pubkey)
|
||||
fingerprint = str(key.pubkey.fingerprint)
|
||||
return private_key_armored, public_key_armored, fingerprint
|
||||
|
||||
@staticmethod
|
||||
def get_server_public_key(server_state: Optional[Dict[str, Any]] = None) -> Optional[pgpy.PGPKey]:
|
||||
"""Retrieves or parses the server's OpenPGP public key for encryption operations."""
|
||||
state = server_state or {}
|
||||
pub_obj = state.get("public_key_obj")
|
||||
if pub_obj:
|
||||
return pub_obj
|
||||
priv_obj = state.get("private_key_obj")
|
||||
if priv_obj and hasattr(priv_obj, "pubkey"):
|
||||
state["public_key_obj"] = priv_obj.pubkey
|
||||
return priv_obj.pubkey
|
||||
pub_blob = state.get("config", {}).get("public_key")
|
||||
if pub_blob:
|
||||
try:
|
||||
key, _ = pgpy.PGPKey.from_blob(pub_blob)
|
||||
state["public_key_obj"] = key
|
||||
return key
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def encrypt_field_for_storage(text: str, force: bool = False, server_state: Optional[Dict[str, Any]] = None, is_unlocked: bool = False) -> str:
|
||||
"""Encrypts a text payload using the server's OpenPGP public key if encrypt_at_rest is enabled."""
|
||||
if not text or not isinstance(text, str):
|
||||
return text
|
||||
state = server_state or {}
|
||||
config = state.get("config", {})
|
||||
should_encrypt = force or (is_unlocked and bool(config.get("encrypt_at_rest", False)))
|
||||
if not should_encrypt:
|
||||
return text
|
||||
|
||||
if text.startswith("-----BEGIN PGP MESSAGE-----"):
|
||||
return text
|
||||
|
||||
pub_key = StorageCryptoManager.get_server_public_key(server_state=state)
|
||||
if not pub_key:
|
||||
return text
|
||||
|
||||
try:
|
||||
msg = pgpy.PGPMessage.new(text)
|
||||
encrypted = pub_key.encrypt(msg)
|
||||
return str(encrypted)
|
||||
except Exception as e:
|
||||
print(f"[!] Warning: Failed to encrypt field at rest: {e}")
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def decrypt_field_from_storage(text: str, server_state: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""
|
||||
Decrypts an OpenPGP armored message string using the server's private key.
|
||||
Returns cleartext for authorized display and API consumption.
|
||||
"""
|
||||
if not text or not isinstance(text, str):
|
||||
return text
|
||||
if not text.startswith("-----BEGIN PGP MESSAGE-----"):
|
||||
return text
|
||||
|
||||
state = server_state or {}
|
||||
priv_key = state.get("private_key_obj")
|
||||
if not priv_key:
|
||||
priv_blob = state.get("config", {}).get("private_key")
|
||||
if priv_blob:
|
||||
try:
|
||||
priv_key, _ = pgpy.PGPKey.from_blob(priv_blob)
|
||||
state["private_key_obj"] = priv_key
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not priv_key:
|
||||
return text
|
||||
|
||||
try:
|
||||
pgp_msg = pgpy.PGPMessage.from_blob(text)
|
||||
decrypted = priv_key.decrypt(pgp_msg)
|
||||
return str(decrypted.message)
|
||||
except Exception as e:
|
||||
print(f"[!] Warning: Failed to decrypt field from storage: {e}")
|
||||
return text
|
||||
|
||||
|
||||
class StorageEngine:
|
||||
"""Manages database schema initialization, migrations, multi-engine switching, and telemetry stats."""
|
||||
|
||||
DEFAULT_DB_FILE = "logar_state.db"
|
||||
DEFAULT_ADMIN_USER = os.environ.get("LOGAR_ADMIN_USER", "admin")
|
||||
DEFAULT_ADMIN_PASS = os.environ.get("LOGAR_ADMIN_PASS", "admin_logar_2026!")
|
||||
|
||||
@classmethod
|
||||
def init_db(cls, db_path: str, enrollment_secret: Optional[str] = None, max_seats: int = 0) -> str:
|
||||
"""Initializes the SQLite schema for multi-run temporal tracking, client tracking, users, and license quota."""
|
||||
def _execute_schema(target_path: str):
|
||||
target_dir = os.path.dirname(os.path.abspath(target_path))
|
||||
if target_dir:
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
conn = sqlite3.connect(target_path, timeout=10.0)
|
||||
try:
|
||||
conn.execute("PRAGMA journal_mode=WAL;")
|
||||
conn.execute("PRAGMA busy_timeout=5000;")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 1. Active Issues
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS active_issues (
|
||||
fingerprint TEXT PRIMARY KEY,
|
||||
site_name TEXT,
|
||||
server TEXT,
|
||||
signature TEXT,
|
||||
severity TEXT,
|
||||
message TEXT,
|
||||
os_type TEXT,
|
||||
first_seen TEXT,
|
||||
last_seen TEXT,
|
||||
run_count INTEGER,
|
||||
status TEXT,
|
||||
last_run_id TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
# 2. Ingest Runs
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ingest_runs (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
site_name TEXT,
|
||||
server TEXT,
|
||||
timestamp TEXT,
|
||||
log_count INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# 3. License Config
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS license_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
max_seats INTEGER NOT NULL DEFAULT 0,
|
||||
enrollment_secret TEXT NOT NULL,
|
||||
license_id TEXT,
|
||||
license_server_url TEXT,
|
||||
issued_at TEXT,
|
||||
expires_at TEXT,
|
||||
license_status TEXT DEFAULT 'unlicensed'
|
||||
)
|
||||
""")
|
||||
|
||||
# Migrations for license_config
|
||||
for col, col_def in [
|
||||
("license_id", "TEXT"),
|
||||
("license_server_url", "TEXT"),
|
||||
("issued_at", "TEXT"),
|
||||
("expires_at", "TEXT"),
|
||||
("license_status", "TEXT DEFAULT 'unlicensed'"),
|
||||
("features", "TEXT DEFAULT '[]'")
|
||||
]:
|
||||
try:
|
||||
conn.execute(f"ALTER TABLE license_config ADD COLUMN {col} {col_def}")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# 4. Clients Table
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
client_id TEXT PRIMARY KEY,
|
||||
hostname TEXT NOT NULL,
|
||||
os_type TEXT NOT NULL,
|
||||
cert_fingerprint TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'active',
|
||||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Migrations for clients table
|
||||
for col, col_def in [
|
||||
("metrics_json", "TEXT DEFAULT '{}'"),
|
||||
("cpu_percent", "REAL DEFAULT 0.0"),
|
||||
("memory_percent", "REAL DEFAULT 0.0"),
|
||||
("disk_percent_free", "REAL DEFAULT 0.0")
|
||||
]:
|
||||
try:
|
||||
conn.execute(f"ALTER TABLE clients ADD COLUMN {col} {col_def}")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# 5. Users Table
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
username TEXT PRIMARY KEY,
|
||||
password_hash TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'admin',
|
||||
created_at TEXT NOT NULL,
|
||||
totp_secret TEXT,
|
||||
totp_enabled INTEGER DEFAULT 0
|
||||
)
|
||||
""")
|
||||
|
||||
# Migrations for users table (2FA)
|
||||
for col, col_def in [
|
||||
("totp_secret", "TEXT"),
|
||||
("totp_enabled", "INTEGER DEFAULT 0")
|
||||
]:
|
||||
try:
|
||||
conn.execute(f"ALTER TABLE users ADD COLUMN {col} {col_def}")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# Ensure default admin user exists
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT COUNT(*) FROM users")
|
||||
if c.fetchone()[0] == 0:
|
||||
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
pwd_hash, salt = PasswordManager.hash_password(cls.DEFAULT_ADMIN_PASS)
|
||||
conn.execute(
|
||||
"INSERT INTO users (username, password_hash, salt, role, created_at) VALUES (?, ?, ?, 'admin', ?)",
|
||||
(cls.DEFAULT_ADMIN_USER, pwd_hash, salt, now_iso)
|
||||
)
|
||||
|
||||
# 6. Hermes Tokens Table
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS hermes_tokens (
|
||||
token TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_used TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
)
|
||||
""")
|
||||
|
||||
# Auto-initialize initial Hermes token if table is empty
|
||||
c.execute("SELECT COUNT(*) FROM hermes_tokens")
|
||||
if c.fetchone()[0] == 0:
|
||||
initial_token = f"hermes_live_{secrets.token_hex(20)}"
|
||||
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
conn.execute(
|
||||
"INSERT INTO hermes_tokens (token, name, created_at, status) VALUES (?, 'Default Hermes Agent', ?, 'active')",
|
||||
(initial_token, now_iso)
|
||||
)
|
||||
|
||||
# 7. Rate Limits & Server Meta Tables
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS rate_limits (
|
||||
ip TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
attempt_time REAL NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_rate_limits_ip_scope ON rate_limits (ip, scope, attempt_time)")
|
||||
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS server_meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
status = "active" if max_seats > 0 else "unlicensed"
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO license_config (id, max_seats, enrollment_secret, license_status, features)
|
||||
VALUES (1, ?, ?, ?, '[]')
|
||||
""", (max_seats, enrollment_secret or "", status))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
_execute_schema(db_path)
|
||||
return db_path
|
||||
except (sqlite3.OperationalError, PermissionError, OSError) as e:
|
||||
if "readonly" in str(e).lower() or "permission" in str(e).lower() or "locked" in str(e).lower() or isinstance(e, PermissionError):
|
||||
local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
|
||||
fallback_dir = os.path.join(local_app, "LOGAR-Server")
|
||||
os.makedirs(fallback_dir, exist_ok=True)
|
||||
fallback_path = os.path.join(fallback_dir, "logar_state.db")
|
||||
if fallback_path != db_path:
|
||||
print(f"[!] Warning: Database path '{db_path}' is not writable ({e}). Falling back to '{fallback_path}'...")
|
||||
_execute_schema(fallback_path)
|
||||
return fallback_path
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def init_postgres_db(pg_config: Dict[str, Any]) -> Tuple[bool, str]:
|
||||
"""Initializes table schema on a PostgreSQL database instance."""
|
||||
try:
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
try:
|
||||
import psycopg as psycopg2
|
||||
except ImportError:
|
||||
return False, "PostgreSQL driver not installed on server (install psycopg2-binary or psycopg)"
|
||||
|
||||
host = pg_config.get("host", "localhost")
|
||||
port = int(pg_config.get("port", 5432))
|
||||
database = pg_config.get("database", "logar")
|
||||
user = pg_config.get("user", "postgres")
|
||||
password = pg_config.get("password", "")
|
||||
sslmode = pg_config.get("sslmode", "prefer")
|
||||
timeout = int(pg_config.get("connect_timeout", 5))
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=host, port=port, dbname=database, user=user, password=password, sslmode=sslmode, connect_timeout=timeout
|
||||
)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS active_issues (
|
||||
fingerprint VARCHAR(255) PRIMARY KEY,
|
||||
site_name VARCHAR(255),
|
||||
server VARCHAR(255),
|
||||
signature TEXT,
|
||||
severity VARCHAR(50),
|
||||
message TEXT,
|
||||
os_type VARCHAR(50),
|
||||
first_seen VARCHAR(50),
|
||||
last_seen VARCHAR(50),
|
||||
run_count INTEGER,
|
||||
status VARCHAR(50),
|
||||
last_run_id VARCHAR(100)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ingest_runs (
|
||||
run_id VARCHAR(100) PRIMARY KEY,
|
||||
site_name VARCHAR(255),
|
||||
server VARCHAR(255),
|
||||
timestamp VARCHAR(50),
|
||||
log_count INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS license_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
max_seats INTEGER NOT NULL DEFAULT 0,
|
||||
enrollment_secret TEXT NOT NULL,
|
||||
license_id VARCHAR(100),
|
||||
license_server_url VARCHAR(255),
|
||||
issued_at VARCHAR(50),
|
||||
expires_at VARCHAR(50),
|
||||
license_status VARCHAR(50) DEFAULT 'unlicensed',
|
||||
features TEXT DEFAULT '[]'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
client_id VARCHAR(100) PRIMARY KEY,
|
||||
hostname VARCHAR(255) NOT NULL,
|
||||
os_type VARCHAR(50) NOT NULL,
|
||||
cert_fingerprint VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'active',
|
||||
first_seen VARCHAR(50),
|
||||
last_seen VARCHAR(50)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
username VARCHAR(100) PRIMARY KEY,
|
||||
password_hash TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'admin',
|
||||
created_at VARCHAR(50) NOT NULL,
|
||||
totp_secret TEXT,
|
||||
totp_enabled INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS hermes_tokens (
|
||||
token VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at VARCHAR(50) NOT NULL,
|
||||
last_used VARCHAR(50),
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS rate_limits (
|
||||
ip VARCHAR(100) NOT NULL,
|
||||
scope VARCHAR(50) NOT NULL,
|
||||
attempt_time DOUBLE PRECISION NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS server_meta (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
# Migrations for Postgres clients table
|
||||
for col, col_def in [
|
||||
("metrics_json", "TEXT DEFAULT '{}'"),
|
||||
("cpu_percent", "DOUBLE PRECISION DEFAULT 0.0"),
|
||||
("memory_percent", "DOUBLE PRECISION DEFAULT 0.0"),
|
||||
("disk_percent_free", "DOUBLE PRECISION DEFAULT 0.0")
|
||||
]:
|
||||
try:
|
||||
cur.execute(f"ALTER TABLE clients ADD COLUMN IF NOT EXISTS {col} {col_def}")
|
||||
except Exception:
|
||||
pass
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, "PostgreSQL schema successfully initialized"
|
||||
except Exception as e:
|
||||
return False, f"PostgreSQL connection/init failed: {e}"
|
||||
|
||||
@staticmethod
|
||||
def migrate_sqlite_to_postgres(
|
||||
sqlite_path: str,
|
||||
pg_config: Dict[str, Any],
|
||||
encrypt_fn: Optional[Callable[[str], str]] = None
|
||||
) -> Tuple[bool, str, Dict[str, int]]:
|
||||
"""Transfers all tables and records from SQLite to PostgreSQL with data validation and encryption."""
|
||||
if not os.path.exists(sqlite_path):
|
||||
return False, f"SQLite database not found at {sqlite_path}", {}
|
||||
|
||||
ok, init_msg = StorageEngine.init_postgres_db(pg_config)
|
||||
if not ok:
|
||||
return False, init_msg, {}
|
||||
|
||||
try:
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
import psycopg as psycopg2
|
||||
|
||||
host = pg_config.get("host", "localhost")
|
||||
port = int(pg_config.get("port", 5432))
|
||||
database = pg_config.get("database", "logar")
|
||||
user = pg_config.get("user", "postgres")
|
||||
password = pg_config.get("password", "")
|
||||
sslmode = pg_config.get("sslmode", "prefer")
|
||||
timeout = int(pg_config.get("connect_timeout", 5))
|
||||
|
||||
pg_conn = psycopg2.connect(
|
||||
host=host, port=port, dbname=database, user=user, password=password, sslmode=sslmode, connect_timeout=timeout
|
||||
)
|
||||
sqlite_conn = sqlite3.connect(sqlite_path)
|
||||
sqlite_conn.row_factory = sqlite3.Row
|
||||
|
||||
migrated_counts = {}
|
||||
tables = ["active_issues", "ingest_runs", "license_config", "clients", "users", "hermes_tokens", "rate_limits", "server_meta"]
|
||||
|
||||
with pg_conn.cursor() as pg_cur:
|
||||
for tbl in tables:
|
||||
try:
|
||||
s_cur = sqlite_conn.cursor()
|
||||
s_cur.execute(f"SELECT * FROM {tbl}")
|
||||
rows = s_cur.fetchall()
|
||||
if rows:
|
||||
col_names = [col[0] for col in s_cur.description]
|
||||
cols_joined = ", ".join(col_names)
|
||||
placeholders = ", ".join(["%s"] * len(col_names))
|
||||
insert_query = f"INSERT INTO {tbl} ({cols_joined}) VALUES ({placeholders}) ON CONFLICT DO NOTHING;"
|
||||
data = []
|
||||
for r in rows:
|
||||
row_data = [r[c] for c in col_names]
|
||||
if tbl == "active_issues" and "message" in col_names and encrypt_fn:
|
||||
msg_idx = col_names.index("message")
|
||||
row_data[msg_idx] = encrypt_fn(row_data[msg_idx])
|
||||
data.append(row_data)
|
||||
|
||||
pg_cur.executemany(insert_query, data)
|
||||
migrated_counts[tbl] = len(rows)
|
||||
else:
|
||||
migrated_counts[tbl] = 0
|
||||
except Exception as tbl_err:
|
||||
print(f"[!] Warning migrating table {tbl}: {tbl_err}")
|
||||
migrated_counts[tbl] = 0
|
||||
|
||||
pg_conn.commit()
|
||||
|
||||
sqlite_conn.close()
|
||||
pg_conn.close()
|
||||
return True, "Migration completed successfully", migrated_counts
|
||||
except Exception as e:
|
||||
return False, f"Migration failed: {e}", {}
|
||||
|
||||
@staticmethod
|
||||
def get_storage_engine_info(
|
||||
db_path: Optional[str] = None,
|
||||
server_state: Optional[Dict[str, Any]] = None,
|
||||
ingest_queue: Optional[Any] = None,
|
||||
is_unlocked: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Returns metadata about the active storage backend, configuration, and database telemetry."""
|
||||
state = server_state or {}
|
||||
config = state.get("config", {})
|
||||
db_type = config.get("db_type", config.get("storage_engine", "sqlite")).lower()
|
||||
target_db = db_path or config.get("db_path", StorageEngine.DEFAULT_DB_FILE)
|
||||
|
||||
table_stats = {"active_issues": 0, "ingest_runs": 0, "clients": 0, "users": 0}
|
||||
|
||||
licensed_features = []
|
||||
if target_db and os.path.exists(target_db):
|
||||
try:
|
||||
conn_l = sqlite3.connect(target_db, timeout=3.0)
|
||||
row_l = conn_l.execute("SELECT features FROM license_config WHERE id = 1").fetchone()
|
||||
conn_l.close()
|
||||
if row_l and row_l[0]:
|
||||
feat_raw = row_l[0]
|
||||
if isinstance(feat_raw, str):
|
||||
if feat_raw.startswith("["):
|
||||
licensed_features = json.loads(feat_raw)
|
||||
else:
|
||||
licensed_features = [f.strip() for f in feat_raw.split(",") if f.strip()]
|
||||
elif isinstance(feat_raw, list):
|
||||
licensed_features = list(feat_raw)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
flush_interval = ingest_queue.flush_interval if ingest_queue else 2.0
|
||||
batch_size = ingest_queue.batch_size if ingest_queue else 100
|
||||
max_queue = ingest_queue.max_queue if ingest_queue else 50000
|
||||
|
||||
if db_type in ("postgres", "postgresql"):
|
||||
pg_cfg = config.get("postgres", {})
|
||||
host = pg_cfg.get("host", "localhost")
|
||||
port = int(pg_cfg.get("port", 5432))
|
||||
dbname = pg_cfg.get("database", "logar")
|
||||
user = pg_cfg.get("user", "postgres")
|
||||
try:
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
import psycopg as psycopg2
|
||||
p_conn = psycopg2.connect(
|
||||
host=host, port=port, dbname=dbname, user=user,
|
||||
password=pg_cfg.get("password", ""),
|
||||
sslmode=pg_cfg.get("sslmode", "prefer"),
|
||||
connect_timeout=int(pg_cfg.get("connect_timeout", 3))
|
||||
)
|
||||
with p_conn.cursor() as p_cur:
|
||||
for tbl in ["active_issues", "ingest_runs", "clients", "users"]:
|
||||
try:
|
||||
p_cur.execute(f"SELECT COUNT(*) FROM {tbl}")
|
||||
r = p_cur.fetchone()
|
||||
table_stats[tbl] = r[0] if r else 0
|
||||
except Exception:
|
||||
pass
|
||||
p_conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"engine": "postgres",
|
||||
"engine_type": "postgres",
|
||||
"backend_display": "PostgreSQL Enterprise Pool",
|
||||
"connection_target": f"{user}@{host}:{port}/{dbname}",
|
||||
"status": "Enterprise Connected",
|
||||
"table_stats": table_stats,
|
||||
"is_enterprise": True,
|
||||
"feature_unlocked": is_unlocked,
|
||||
"licensed_features": licensed_features,
|
||||
"cache_mode": config.get("cache_mode", "direct"),
|
||||
"encrypt_at_rest": config.get("encrypt_at_rest", False),
|
||||
"flush_interval": flush_interval,
|
||||
"batch_size": batch_size,
|
||||
"max_queue": max_queue,
|
||||
"postgres_config": pg_cfg
|
||||
}
|
||||
else:
|
||||
size_bytes = 0
|
||||
if target_db and os.path.exists(target_db):
|
||||
try:
|
||||
size_bytes = os.path.getsize(target_db)
|
||||
conn = sqlite3.connect(target_db, timeout=3.0)
|
||||
for tbl in ["active_issues", "ingest_runs", "clients", "users"]:
|
||||
try:
|
||||
row = conn.execute(f"SELECT COUNT(*) FROM {tbl}").fetchone()
|
||||
table_stats[tbl] = row[0] if row else 0
|
||||
except Exception:
|
||||
pass
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
size_kb = round(size_bytes / 1024, 2)
|
||||
return {
|
||||
"engine": "sqlite",
|
||||
"engine_type": "sqlite",
|
||||
"backend_display": "SQLite 3 (WAL Mode)",
|
||||
"connection_target": target_db,
|
||||
"db_size_kb": size_kb,
|
||||
"status": "Local WAL Optimized",
|
||||
"table_stats": table_stats,
|
||||
"is_enterprise": False,
|
||||
"feature_unlocked": is_unlocked,
|
||||
"licensed_features": licensed_features,
|
||||
"cache_mode": config.get("cache_mode", "direct"),
|
||||
"encrypt_at_rest": config.get("encrypt_at_rest", False),
|
||||
"flush_interval": flush_interval,
|
||||
"batch_size": batch_size,
|
||||
"max_queue": max_queue,
|
||||
"postgres_config": config.get("postgres", {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"database": "logar",
|
||||
"user": "postgres",
|
||||
"password": "",
|
||||
"sslmode": "prefer",
|
||||
"connect_timeout": 5,
|
||||
"pool_size": 10
|
||||
})
|
||||
}
|
||||
+1680
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user