CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m24s
995 lines
46 KiB
Python
995 lines
46 KiB
Python
import os
|
|
import sys
|
|
import json
|
|
import socket
|
|
import struct
|
|
import sqlite3
|
|
import unittest
|
|
import urllib.request
|
|
import uuid
|
|
import warnings
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
warnings.filterwarnings("ignore")
|
|
|
|
# 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__), "..", "src")))
|
|
|
|
import Server
|
|
import pgpy
|
|
|
|
|
|
class TestServerComponent(unittest.TestCase):
|
|
def setUp(self):
|
|
self.test_db = f"test_server_state_{uuid.uuid4().hex[:8]}.db"
|
|
self.test_config = f"test_server_config_{uuid.uuid4().hex[:8]}.json"
|
|
|
|
def tearDown(self):
|
|
if os.path.exists(self.test_db):
|
|
try:
|
|
os.remove(self.test_db)
|
|
except Exception:
|
|
pass
|
|
if os.path.exists(self.test_config):
|
|
try:
|
|
os.remove(self.test_config)
|
|
except Exception:
|
|
pass
|
|
|
|
def test_first_run_config_and_keypair_generation(self):
|
|
config = Server.load_or_init_config(self.test_config)
|
|
self.assertTrue(os.path.exists(self.test_config))
|
|
self.assertIn("server_fingerprint", config)
|
|
self.assertIn("public_key", config)
|
|
self.assertIn("private_key", config)
|
|
self.assertIn("auth_token", config)
|
|
self.assertNotIn("site_name", config)
|
|
|
|
# Verify keypair
|
|
priv_key, _ = pgpy.PGPKey.from_blob(config["private_key"])
|
|
pub_key, _ = pgpy.PGPKey.from_blob(config["public_key"])
|
|
self.assertEqual(str(pub_key.fingerprint), config["server_fingerprint"])
|
|
|
|
def test_create_client_config(self):
|
|
Server.load_or_init_config(self.test_config)
|
|
client_out = "test_client_out.json"
|
|
try:
|
|
client_conf = Server.create_client_config(
|
|
server_host="10.0.0.1",
|
|
server_port=9443,
|
|
output_path=client_out,
|
|
config_path=self.test_config
|
|
)
|
|
self.assertTrue(os.path.exists(client_out))
|
|
self.assertEqual(client_conf["server_host"], "10.0.0.1")
|
|
self.assertEqual(client_conf["server_port"], 9443)
|
|
# Verify no machine name or site_name is included
|
|
self.assertNotIn("server_name", client_conf)
|
|
self.assertNotIn("name", client_conf)
|
|
self.assertNotIn("site_name", client_conf)
|
|
finally:
|
|
if os.path.exists(client_out):
|
|
os.remove(client_out)
|
|
|
|
def test_direct_hermes_anomaly_and_12h_window(self):
|
|
Server.init_db(self.test_db)
|
|
log_entry = {
|
|
"server": "app-worker-01.corp.local",
|
|
"signature": "PostgresConnWarning",
|
|
"severity": "WARNING",
|
|
"message": "Connection to database pool near capacity: 85%",
|
|
"os_type": "linux"
|
|
}
|
|
payload = {
|
|
"server": "app-worker-01.corp.local",
|
|
"logs": [log_entry]
|
|
}
|
|
|
|
# Run 1: WARNING is immediately VERIFIED for Hermes AI reasoning
|
|
res1 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
|
|
self.assertEqual(res1["status"], "success")
|
|
self.assertEqual(res1["promoted_verified"], 1)
|
|
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
|
|
row = c.fetchone()
|
|
conn.close()
|
|
self.assertEqual(row[0], 1)
|
|
self.assertEqual(row[1], "VERIFIED")
|
|
|
|
# Runs 2 to 4: Increments consecutive run count while remaining VERIFIED
|
|
for run_idx in range(2, 5):
|
|
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
|
|
self.assertEqual(res["status"], "success")
|
|
self.assertEqual(res["promoted_verified"], 0)
|
|
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
|
|
row = c.fetchone()
|
|
conn.close()
|
|
self.assertEqual(row[0], 4)
|
|
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=1)
|
|
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):
|
|
Server.init_db(self.test_db)
|
|
payload = {
|
|
"server": "app-worker-01.corp.local",
|
|
"logs": [
|
|
{"server": "app-worker-01", "signature": "SigInfo", "severity": "INFO", "message": "Info msg", "os_type": "linux"},
|
|
{"server": "app-worker-01", "signature": "SigWarn", "severity": "WARNING", "message": "Warn msg", "os_type": "linux"},
|
|
{"server": "app-worker-01", "signature": "SigErr", "severity": "ERROR", "message": "Err msg", "os_type": "linux"},
|
|
{"server": "app-worker-01", "signature": "SigDebug", "severity": "DEBUG", "message": "Debug msg", "os_type": "linux"},
|
|
{"server": "app-worker-01", "signature": "SigTrace", "severity": "TRACE", "message": "Trace msg", "os_type": "linux"}
|
|
]
|
|
}
|
|
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=1)
|
|
self.assertEqual(res["status"], "success")
|
|
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT signature, status FROM active_issues ORDER BY signature")
|
|
rows = dict(c.fetchall())
|
|
conn.close()
|
|
|
|
self.assertIn("SigInfo", rows)
|
|
self.assertIn("SigWarn", rows)
|
|
self.assertIn("SigErr", rows)
|
|
self.assertNotIn("SigDebug", rows)
|
|
self.assertNotIn("SigTrace", rows)
|
|
|
|
# All actionable issues (ERROR, WARNING, INFO) are promoted to VERIFIED immediately for Hermes reasoning
|
|
self.assertEqual(rows["SigErr"], "VERIFIED")
|
|
self.assertEqual(rows["SigWarn"], "VERIFIED")
|
|
self.assertEqual(rows["SigInfo"], "VERIFIED")
|
|
|
|
def test_license_schema_and_pki_generation(self):
|
|
secret = "test-secret-12345"
|
|
Server.init_db(self.test_db, enrollment_secret=secret, max_seats=5)
|
|
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT max_seats, enrollment_secret FROM license_config WHERE id = 1")
|
|
row = c.fetchone()
|
|
conn.close()
|
|
|
|
self.assertIsNotNone(row)
|
|
self.assertEqual(row[0], 5)
|
|
self.assertEqual(row[1], secret)
|
|
|
|
# Test Dynamic PKI
|
|
test_cert_dir = "test_certs_pki"
|
|
try:
|
|
ca_cert, ca_key, ca_pem, ca_key_pem = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
|
|
self.assertIn("BEGIN CERTIFICATE", ca_pem)
|
|
self.assertIn("BEGIN RSA PRIVATE KEY", ca_key_pem)
|
|
|
|
srv_cert, srv_key, srv_pem, srv_key_pem = Server.enrollment.generate_server_cert_if_needed(
|
|
ca_cert, ca_key, hostnames=["127.0.0.1", "localhost"], cert_dir=test_cert_dir
|
|
)
|
|
self.assertIn("BEGIN CERTIFICATE", srv_pem)
|
|
|
|
client_cert_pem, client_key_pem = Server.enrollment.issue_client_cert("node-test-1", ca_cert, ca_key)
|
|
self.assertIn("BEGIN CERTIFICATE", client_cert_pem)
|
|
self.assertIn("BEGIN RSA PRIVATE KEY", client_key_pem)
|
|
|
|
fp = Server.enrollment.calculate_cert_fingerprint(client_cert_pem)
|
|
self.assertEqual(len(fp), 64)
|
|
finally:
|
|
import shutil
|
|
if os.path.exists(test_cert_dir):
|
|
shutil.rmtree(test_cert_dir, ignore_errors=True)
|
|
|
|
def test_enrollment_endpoint_and_seat_quota(self):
|
|
from fastapi import HTTPException
|
|
secret = "super-secret-enrollment"
|
|
max_seats = 2
|
|
Server.init_db(self.test_db, enrollment_secret=secret, max_seats=max_seats)
|
|
|
|
test_cert_dir = "test_certs_enroll"
|
|
try:
|
|
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
|
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": test_cert_dir}
|
|
Server.SERVER_STATE["ca_cert"] = ca_cert
|
|
Server.SERVER_STATE["ca_key"] = ca_key
|
|
Server.SERVER_STATE["ca_cert_pem"] = ca_pem
|
|
|
|
# 1. Invalid secret should raise 403
|
|
bad_req = Server.ClientEnrollRequest(
|
|
client_id="client-1",
|
|
hostname="host-1",
|
|
os="linux",
|
|
enrollment_secret="wrong-secret"
|
|
)
|
|
with self.assertRaises(HTTPException) as cm:
|
|
Server.enroll_client(bad_req)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
|
|
# 2. Valid enrollment for client 1
|
|
req1 = Server.ClientEnrollRequest(
|
|
client_id="client-1",
|
|
hostname="host-1",
|
|
os="linux",
|
|
enrollment_secret=secret
|
|
)
|
|
resp1 = Server.enroll_client(req1)
|
|
self.assertIn("client_cert", resp1)
|
|
self.assertIn("client_key", resp1)
|
|
self.assertTrue(resp1["ca_cert"].strip().startswith(ca_pem.strip()))
|
|
|
|
# 3. Valid enrollment for client 2
|
|
req2 = Server.ClientEnrollRequest(
|
|
client_id="client-2",
|
|
hostname="host-2",
|
|
os="windows",
|
|
enrollment_secret=secret
|
|
)
|
|
resp2 = Server.enroll_client(req2)
|
|
self.assertIn("client_cert", resp2)
|
|
|
|
# 4. Seat quota exhausted: client 3 should raise 403
|
|
req3 = Server.ClientEnrollRequest(
|
|
client_id="client-3",
|
|
hostname="host-3",
|
|
os="linux",
|
|
enrollment_secret=secret
|
|
)
|
|
with self.assertRaises(HTTPException) as cm:
|
|
Server.enroll_client(req3)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
self.assertIn("License seat limit reached", cm.exception.detail)
|
|
|
|
# 5. Re-enrollment for existing client 1 should succeed
|
|
resp1_re = Server.enroll_client(req1)
|
|
self.assertIn("client_cert", resp1_re)
|
|
|
|
# 6. Revoked client should be rejected
|
|
conn = sqlite3.connect(self.test_db)
|
|
conn.execute("UPDATE clients SET status = 'revoked' WHERE client_id = 'client-1'")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
with self.assertRaises(HTTPException) as cm:
|
|
Server.enroll_client(req1)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
self.assertIn("revoked", cm.exception.detail)
|
|
finally:
|
|
import shutil
|
|
if os.path.exists(test_cert_dir):
|
|
shutil.rmtree(test_cert_dir, ignore_errors=True)
|
|
|
|
def test_cert_validity_and_hub_pki_renewal(self):
|
|
test_cert_dir = "test_certs_renew"
|
|
try:
|
|
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
|
|
srv_cert, srv_key, srv_pem, _ = Server.enrollment.generate_server_cert_if_needed(
|
|
ca_cert, ca_key, hostnames=["127.0.0.1"], cert_dir=test_cert_dir
|
|
)
|
|
|
|
# 1. Freshly generated certificates should NOT be expiring soon with standard 30-day threshold
|
|
self.assertFalse(Server.enrollment.is_cert_expiring_soon(ca_pem, threshold_days=30))
|
|
self.assertFalse(Server.enrollment.is_cert_expiring_soon(srv_pem, threshold_days=30))
|
|
|
|
# 2. Huge threshold (e.g. 5000 days) should flag expiration
|
|
self.assertTrue(Server.enrollment.is_cert_expiring_soon(srv_pem, threshold_days=5000))
|
|
|
|
# 3. check_and_renew_hub_pki with standard threshold should report no renewal needed
|
|
ca_renewed, srv_renewed = Server.enrollment.check_and_renew_hub_pki(cert_dir=test_cert_dir, threshold_days=30)
|
|
self.assertFalse(ca_renewed)
|
|
self.assertFalse(srv_renewed)
|
|
|
|
# 4. In-flight reload of SSLContext
|
|
ssl_ctx = Server.init_mtls_server_context(cert_dir=test_cert_dir)
|
|
Server.SERVER_STATE["ssl_ctx"] = ssl_ctx
|
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": test_cert_dir, "tcp_host": "127.0.0.1"}
|
|
|
|
# Trigger rotation using high threshold
|
|
rotated = Server.check_and_rotate_server_certs(cert_dir=test_cert_dir, hostnames=["127.0.0.1"], threshold_days=5000)
|
|
self.assertTrue(rotated)
|
|
|
|
# Check that backup files were generated
|
|
bak_files = [f for f in os.listdir(test_cert_dir) if f.endswith(".bak")]
|
|
self.assertGreater(len(bak_files), 0)
|
|
finally:
|
|
import shutil
|
|
if os.path.exists(test_cert_dir):
|
|
shutil.rmtree(test_cert_dir, ignore_errors=True)
|
|
|
|
def test_user_auth_and_session(self):
|
|
"""Tests admin password hashing, verification, session tokens, and password change."""
|
|
Server.init_db(self.test_db)
|
|
|
|
# Verify default admin credentials
|
|
self.assertTrue(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
|
|
self.assertFalse(Server.verify_user_credentials("admin", "wrong_password", db_path=self.test_db))
|
|
self.assertFalse(Server.verify_user_credentials("nonexistent", "password", db_path=self.test_db))
|
|
|
|
# Test session token generation & validation
|
|
token = Server.generate_session_token("admin")
|
|
self.assertEqual(Server.verify_session_token(token), "admin")
|
|
self.assertIsNone(Server.verify_session_token("invalid.token.structure"))
|
|
|
|
# Test changing password
|
|
Server.change_user_password("admin", "new_secure_pass123", db_path=self.test_db)
|
|
self.assertFalse(Server.verify_user_credentials("admin", "admin_logar_2026!", db_path=self.test_db))
|
|
self.assertTrue(Server.verify_user_credentials("admin", "new_secure_pass123", db_path=self.test_db))
|
|
|
|
def test_hermes_tokens_and_auth(self):
|
|
"""Tests Hermes Bearer token creation, verification, revocation, and deletion."""
|
|
Server.init_db(self.test_db)
|
|
|
|
# Test default Hermes token was created on init
|
|
tokens = Server.list_hermes_tokens(db_path=self.test_db)
|
|
self.assertEqual(len(tokens), 1)
|
|
self.assertEqual(tokens[0]["status"], "active")
|
|
default_token = tokens[0]["token"]
|
|
self.assertTrue(Server.verify_hermes_token(default_token, db_path=self.test_db))
|
|
|
|
# Test creating new token
|
|
new_tok = Server.create_hermes_token(name="Hermes Autonomous Agent 01", db_path=self.test_db)
|
|
self.assertIn("token", new_tok)
|
|
self.assertEqual(new_tok["name"], "Hermes Autonomous Agent 01")
|
|
self.assertTrue(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
|
|
|
|
# Test revoking token
|
|
self.assertTrue(Server.revoke_hermes_token(new_tok["token"], db_path=self.test_db))
|
|
self.assertFalse(Server.verify_hermes_token(new_tok["token"], db_path=self.test_db))
|
|
|
|
# Test deleting token
|
|
self.assertTrue(Server.delete_hermes_token(new_tok["token"], db_path=self.test_db))
|
|
tokens_after = Server.list_hermes_tokens(db_path=self.test_db)
|
|
self.assertEqual(len(tokens_after), 1)
|
|
|
|
def test_client_profile_export(self):
|
|
"""Tests client profile dictionary generation and export formatting."""
|
|
cfg = Server.load_or_init_config(self.test_config)
|
|
profile = Server.generate_client_profile_dict(
|
|
server_host="logar-hub.corp.internal",
|
|
server_port=9443,
|
|
client_id="worker-node-101",
|
|
config_dict=cfg
|
|
)
|
|
self.assertEqual(profile["server_host"], "logar-hub.corp.internal")
|
|
self.assertEqual(profile["server_port"], 9443)
|
|
self.assertEqual(profile["client_id"], "worker-node-101")
|
|
self.assertEqual(profile["server_fingerprint"], cfg["server_fingerprint"])
|
|
self.assertEqual(profile["server_public_key"], cfg["public_key"])
|
|
self.assertEqual(profile["enrollment_secret"], cfg["enrollment_secret"])
|
|
self.assertEqual(profile["format"], "LOGAR_CLIENT_PROFILE")
|
|
|
|
def test_client_seat_management_and_relative_time(self):
|
|
"""Tests client listing, seat approval, revocation, deletion, and relative timestamp helper."""
|
|
Server.init_db(self.test_db, enrollment_secret="test-sec-123", max_seats=5)
|
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db}
|
|
|
|
# Populate test clients
|
|
conn = sqlite3.connect(self.test_db)
|
|
now = datetime.now(timezone.utc)
|
|
five_min_ago = (now - timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S")
|
|
two_hours_ago = (now - timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-win', 'WIN-PROD-01', 'windows', 'FP_W1', 'active', ?)", (five_min_ago,))
|
|
conn.execute("INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen) VALUES ('c-lin', 'LIN-PROD-01', 'linux', 'FP_L1', 'active', ?)", (two_hours_ago,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# 1. Test listing
|
|
client_data = Server.list_clients()
|
|
self.assertEqual(client_data["active_seats"], 2)
|
|
self.assertEqual(client_data["max_seats"], 5)
|
|
self.assertEqual(len(client_data["clients"]), 2)
|
|
self.assertEqual(client_data["clients"][0]["last_activity_relative"], "5m ago")
|
|
|
|
# 2. Test revoking
|
|
rev_req = Server.ClientActionRequest(client_id="c-win")
|
|
rev_res = Server.revoke_client(rev_req)
|
|
self.assertEqual(rev_res["status"], "revoked")
|
|
client_data = Server.list_clients()
|
|
self.assertEqual(client_data["active_seats"], 1)
|
|
|
|
# 3. Test approving / reactivating
|
|
app_res = Server.approve_client(rev_req)
|
|
self.assertEqual(app_res["status"], "active")
|
|
client_data = Server.list_clients()
|
|
self.assertEqual(client_data["active_seats"], 2)
|
|
|
|
# 4. Test deleting client record
|
|
del_res = Server.delete_client(rev_req)
|
|
self.assertEqual(del_res["status"], "deleted")
|
|
client_data = Server.list_clients()
|
|
self.assertEqual(len(client_data["clients"]), 1)
|
|
self.assertEqual(client_data["active_seats"], 1)
|
|
|
|
# 5. Test format_relative_time edge cases
|
|
self.assertEqual(Server.format_relative_time(None), "Never")
|
|
self.assertEqual(Server.format_relative_time(""), "Never")
|
|
self.assertEqual(Server.format_relative_time("invalid-date-string"), "invalid-date-str")
|
|
now_fresh = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
rel = Server.format_relative_time(now_fresh)
|
|
self.assertTrue(rel.endswith("s ago") or rel == "Just now", f"Unexpected relative time: {rel}")
|
|
|
|
def test_integrated_local_log_collector_zero_seats(self):
|
|
"""Tests that local hub log collection runs without license, registering a 0-seat built-in client."""
|
|
Server.init_db(self.test_db, enrollment_secret="test-sec-456", max_seats=0)
|
|
Server.SERVER_STATE["config"] = {
|
|
"db_path": self.test_db,
|
|
"evaluation_window_hours": 12,
|
|
"min_persistence_runs": 1
|
|
}
|
|
|
|
# Run cycle
|
|
state = {}
|
|
processed = Server.run_local_log_collection_cycle(self.test_db, window_hours=12, min_runs=1, state=state)
|
|
|
|
# Verify local hub client entry exists in clients table
|
|
machine_id = Server.get_machine_identifier()
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT client_id, cert_fingerprint, status FROM clients WHERE client_id = ?", (machine_id,))
|
|
row = c.fetchone()
|
|
conn.close()
|
|
|
|
self.assertIsNotNone(row)
|
|
self.assertEqual(row[0], machine_id)
|
|
self.assertEqual(row[1], "LOCAL-HOST-BUILTIN")
|
|
self.assertEqual(row[2], "active")
|
|
|
|
# Verify active seat count is STILL 0 (exempt from seat accounting)
|
|
client_data = Server.list_clients()
|
|
self.assertEqual(client_data["active_seats"], 0)
|
|
self.assertEqual(len(client_data["clients"]), 1)
|
|
|
|
def test_two_factor_auth_and_totp_flow(self):
|
|
"""Tests TOTP secret generation, token verification, URI generation, and enable/disable lifecycle."""
|
|
Server.init_db(self.test_db)
|
|
|
|
# 1. Test TOTP helpers
|
|
secret = Server.generate_totp_secret()
|
|
self.assertTrue(len(secret) >= 16)
|
|
token = Server.get_totp_token(secret)
|
|
self.assertEqual(len(token), 6)
|
|
self.assertTrue(token.isdigit())
|
|
self.assertTrue(Server.verify_totp_code(secret, token))
|
|
self.assertFalse(Server.verify_totp_code(secret, "000000" if token != "000000" else "999999"))
|
|
self.assertFalse(Server.verify_totp_code(secret, "invalid"))
|
|
|
|
# 2. Test OTPAuth URI
|
|
uri = Server.get_totp_uri("admin", secret, issuer="LOGAR Fleet Hub")
|
|
self.assertTrue(uri.startswith("otpauth://totp/"))
|
|
self.assertIn("secret=" + secret, uri)
|
|
self.assertIn("LOGAR", uri)
|
|
|
|
# 3. Test initial user 2FA status
|
|
info = Server.get_user_totp_info("admin", db_path=self.test_db)
|
|
self.assertFalse(info["enabled"])
|
|
self.assertEqual(info["secret"], "")
|
|
|
|
# 4. Test enabling 2FA
|
|
enabled = Server.enable_user_totp("admin", secret, db_path=self.test_db)
|
|
self.assertTrue(enabled)
|
|
|
|
info_active = Server.get_user_totp_info("admin", db_path=self.test_db)
|
|
self.assertTrue(info_active["enabled"])
|
|
self.assertEqual(info_active["secret"], secret)
|
|
|
|
# 5. Test disabling 2FA
|
|
disabled = Server.disable_user_totp("admin", db_path=self.test_db)
|
|
self.assertTrue(disabled)
|
|
|
|
info_disabled = Server.get_user_totp_info("admin", db_path=self.test_db)
|
|
self.assertFalse(info_disabled["enabled"])
|
|
|
|
def test_persistent_session_secret_and_rate_limiting(self):
|
|
"""Tests DB persistence of session secret and sliding window rate limiting."""
|
|
Server.init_db(self.test_db)
|
|
|
|
# 1. Session secret persistence
|
|
sec1 = Server.get_session_secret(self.test_db)
|
|
self.assertTrue(len(sec1) >= 32)
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT value FROM server_meta WHERE key = 'session_secret'")
|
|
row = c.fetchone()
|
|
conn.close()
|
|
self.assertIsNotNone(row)
|
|
self.assertEqual(row[0], sec1)
|
|
|
|
# Token generated with test_db
|
|
token = Server.generate_session_token("admin", db_path=self.test_db)
|
|
self.assertEqual(Server.verify_session_token(token, db_path=self.test_db), "admin")
|
|
|
|
# 2. Rate limiting
|
|
test_ip = "10.0.0.99"
|
|
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
|
|
|
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
|
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
|
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
|
|
|
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
|
self.assertFalse(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
|
|
|
# Different scope ('enroll') should still be allowed
|
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="enroll", db_path=self.test_db))
|
|
|
|
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
|
|
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
|
|
|
def test_ingest_queue_manager_and_batch_flush(self):
|
|
"""Tests IngestQueueManager buffering, capacity bounds, and atomic batch flushing."""
|
|
Server.init_db(self.test_db)
|
|
qm = Server.IngestQueueManager(flush_interval=1.0, batch_size=5, max_queue=50)
|
|
|
|
# Initial stats
|
|
stats = qm.get_stats()
|
|
self.assertEqual(stats["queue_size"], 0)
|
|
self.assertEqual(stats["total_ingested"], 0)
|
|
self.assertEqual(stats["total_flushed"], 0)
|
|
|
|
# Push 3 payloads
|
|
for i in range(3):
|
|
payload = {
|
|
"server": f"node-{i}.corp.local",
|
|
"logs": [
|
|
{"server": f"node-{i}", "signature": f"Sig_{i}", "severity": "WARNING", "message": f"Log msg {i}", "os_type": "linux"}
|
|
]
|
|
}
|
|
pushed = qm.push(payload)
|
|
self.assertTrue(pushed)
|
|
|
|
stats = qm.get_stats()
|
|
self.assertEqual(stats["queue_size"], 3)
|
|
self.assertEqual(stats["total_ingested"], 3)
|
|
|
|
# Flush to database
|
|
res_flush = qm.flush(db_path=self.test_db, window_hours=12, min_runs=1)
|
|
self.assertEqual(res_flush["flushed_runs"], 3)
|
|
self.assertEqual(res_flush["processed_logs"], 3)
|
|
|
|
# Verify DB records
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT COUNT(*) FROM ingest_runs")
|
|
run_count = c.fetchone()[0]
|
|
c.execute("SELECT COUNT(*) FROM active_issues WHERE status = 'VERIFIED'")
|
|
issue_count = c.fetchone()[0]
|
|
conn.close()
|
|
|
|
self.assertEqual(run_count, 3)
|
|
self.assertEqual(issue_count, 3)
|
|
|
|
# Stats after flush
|
|
stats = qm.get_stats()
|
|
self.assertEqual(stats["queue_size"], 0)
|
|
self.assertEqual(stats["total_flushed"], 3)
|
|
|
|
def test_enterprise_db_feature_gating(self):
|
|
"""Tests feature check helpers and DB storage info under standard and enterprise licensed states."""
|
|
Server.init_db(self.test_db)
|
|
|
|
# 1. Standard / Unlicensed state
|
|
self.assertFalse(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
|
info = Server.get_storage_engine_info(db_path=self.test_db)
|
|
self.assertEqual(info["engine"], "sqlite")
|
|
self.assertFalse(info["feature_unlocked"])
|
|
self.assertIn("SQLite 3 (WAL Mode)", info["backend_display"])
|
|
|
|
# 2. Unlock enterprise_db feature in DB
|
|
conn = sqlite3.connect(self.test_db)
|
|
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db", "ai_heuristics"]),))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
self.assertTrue(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
|
self.assertTrue(Server.is_feature_licensed("ai_heuristics", db_path=self.test_db))
|
|
self.assertFalse(Server.is_feature_licensed("unknown_feat", db_path=self.test_db))
|
|
|
|
info_unlocked = Server.get_storage_engine_info(db_path=self.test_db)
|
|
self.assertTrue(info_unlocked["feature_unlocked"])
|
|
self.assertIn("enterprise_db", info_unlocked["licensed_features"])
|
|
|
|
def test_postgres_storage_endpoints_authorization(self):
|
|
"""Tests REST API security gating on DB engine management endpoints."""
|
|
Server.init_db(self.test_db)
|
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "storage_engine": "sqlite"}
|
|
|
|
# 1. Test direct authorization & endpoint logic
|
|
stats = Server.api_db_stats()
|
|
self.assertIn("queue", stats)
|
|
self.assertIn("storage", stats)
|
|
self.assertFalse(stats["enterprise_db_licensed"])
|
|
|
|
# 2. Test Connection without enterprise_db -> HTTPException 403
|
|
pg_req = Server.DbConnectionTestRequest(host="localhost", port=5432, database="logar", user="postgres", password="")
|
|
with self.assertRaises(Server.HTTPException) as cm:
|
|
Server.api_db_test_connection(pg_req)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
self.assertIn("enterprise_db", cm.exception.detail)
|
|
|
|
# 3. Switch backend without enterprise_db -> HTTPException 403
|
|
switch_req = Server.DbSwitchBackendRequest(db_type="postgres", host="localhost", port=5432, database="logar", user="postgres", password="")
|
|
with self.assertRaises(Server.HTTPException) as cm:
|
|
Server.api_db_switch_backend(switch_req)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
self.assertIn("enterprise_db", cm.exception.detail)
|
|
|
|
# 4. Flush buffer endpoint executes successfully
|
|
flush_res = Server.api_db_flush_buffer()
|
|
self.assertEqual(flush_res["status"], "success")
|
|
|
|
# 5. Optional HTTP client verification if starlette testclient dependencies are present
|
|
try:
|
|
from fastapi.testclient import TestClient
|
|
client = TestClient(Server.app)
|
|
|
|
# Unauthenticated request to /api/db/stats -> 401
|
|
res = client.get("/api/db/stats")
|
|
self.assertEqual(res.status_code, 401)
|
|
|
|
# Authenticated request with session cookie
|
|
token = Server.generate_session_token("admin", db_path=self.test_db)
|
|
client.cookies.set(Server.SESSION_COOKIE_NAME, token)
|
|
|
|
res_auth = client.get("/api/db/stats")
|
|
self.assertEqual(res_auth.status_code, 200)
|
|
data = res_auth.json()
|
|
self.assertIn("queue", data)
|
|
self.assertIn("storage", data)
|
|
self.assertFalse(data["enterprise_db_licensed"])
|
|
|
|
# 403 checks via HTTP client
|
|
res_test = client.post("/api/db/test-connection", json={"host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
|
|
self.assertEqual(res_test.status_code, 403)
|
|
|
|
res_switch = client.post("/api/db/switch-backend", json={"db_type": "postgres", "host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
|
|
self.assertEqual(res_switch.status_code, 403)
|
|
|
|
res_flush = client.post("/api/db/flush-buffer")
|
|
self.assertEqual(res_flush.status_code, 200)
|
|
except (ImportError, RuntimeError, Exception):
|
|
# Starlette TestClient may raise RuntimeError when httpx/httpx2 is missing in lightweight runner environments
|
|
pass
|
|
|
|
def test_db_encryption_at_rest_and_full_config_api(self):
|
|
"""Tests OpenPGP data-at-rest encryption/decryption and full DB/Cache configuration API."""
|
|
Server.init_db(self.test_db)
|
|
priv_armored, pub_armored, fp = Server.generate_server_keypair("test-db-node")
|
|
priv_key, _ = pgpy.PGPKey.from_blob(priv_armored)
|
|
pub_key, _ = pgpy.PGPKey.from_blob(pub_armored)
|
|
|
|
Server.SERVER_STATE["config"] = {
|
|
"db_path": self.test_db,
|
|
"storage_engine": "sqlite",
|
|
"cache_mode": "direct",
|
|
"encrypt_at_rest": False,
|
|
"private_key": priv_armored,
|
|
"public_key": pub_armored,
|
|
"server_fingerprint": fp
|
|
}
|
|
Server.SERVER_STATE["private_key_obj"] = priv_key
|
|
Server.SERVER_STATE["public_key_obj"] = pub_key
|
|
|
|
# 1. Test OpenPGP encryption & decryption roundtrip
|
|
raw_msg = "Critical database corruption: segment 0x89FA damaged"
|
|
encrypted_msg = Server.encrypt_field_for_storage(raw_msg, force=True)
|
|
self.assertTrue(encrypted_msg.startswith("-----BEGIN PGP MESSAGE-----"))
|
|
self.assertNotIn("Critical database corruption", encrypted_msg)
|
|
|
|
decrypted_msg = Server.decrypt_field_from_storage(encrypted_msg)
|
|
self.assertEqual(decrypted_msg, raw_msg)
|
|
|
|
# 2. Test that without enterprise_db license, buffering, encryption at rest, and postgres are blocked
|
|
cfg_unlicensed_buf = Server.DbFullConfigRequest(
|
|
storage_engine="sqlite",
|
|
cache_mode="buffered",
|
|
encrypt_at_rest=False
|
|
)
|
|
with self.assertRaises(Server.HTTPException) as cm:
|
|
Server.api_db_update_config(cfg_unlicensed_buf)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
self.assertIn("enterprise_db", cm.exception.detail)
|
|
|
|
cfg_unlicensed_enc = Server.DbFullConfigRequest(
|
|
storage_engine="sqlite",
|
|
cache_mode="direct",
|
|
encrypt_at_rest=True
|
|
)
|
|
with self.assertRaises(Server.HTTPException) as cm:
|
|
Server.api_db_update_config(cfg_unlicensed_enc)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
self.assertIn("enterprise_db", cm.exception.detail)
|
|
|
|
pg_req = Server.DbFullConfigRequest(
|
|
storage_engine="postgres",
|
|
cache_mode="direct",
|
|
pg_config=Server.DbConnectionTestRequest(host="db.corp.internal", port=5432, database="logar", user="postgres", password="")
|
|
)
|
|
with self.assertRaises(Server.HTTPException) as cm:
|
|
Server.api_db_update_config(pg_req)
|
|
self.assertEqual(cm.exception.status_code, 403)
|
|
self.assertIn("enterprise_db", cm.exception.detail)
|
|
|
|
# 3. Standard local SQLite configuration succeeds when unlicensed
|
|
cfg_standard = Server.DbFullConfigRequest(
|
|
storage_engine="sqlite",
|
|
cache_mode="direct",
|
|
encrypt_at_rest=False
|
|
)
|
|
res_std = Server.api_db_update_config(cfg_standard)
|
|
self.assertEqual(res_std["status"], "success")
|
|
|
|
# 4. Unlock enterprise_db feature in license_config
|
|
conn = sqlite3.connect(self.test_db)
|
|
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# 5. Test updating config with enterprise_db unlocked (direct -> buffered, encrypt_at_rest = True)
|
|
cfg_req = Server.DbFullConfigRequest(
|
|
storage_engine="sqlite",
|
|
cache_mode="buffered",
|
|
flush_interval=4.0,
|
|
batch_size=200,
|
|
max_queue=25000,
|
|
encrypt_at_rest=True
|
|
)
|
|
res = Server.api_db_update_config(cfg_req)
|
|
self.assertEqual(res["status"], "success")
|
|
self.assertEqual(Server.SERVER_STATE["config"]["cache_mode"], "buffered")
|
|
self.assertTrue(Server.SERVER_STATE["config"]["encrypt_at_rest"])
|
|
self.assertEqual(Server.INGEST_QUEUE.flush_interval, 4.0)
|
|
self.assertEqual(Server.INGEST_QUEUE.batch_size, 200)
|
|
|
|
# 6. Verify storage engine info output
|
|
info = Server.get_storage_engine_info(db_path=self.test_db)
|
|
self.assertEqual(info["cache_mode"], "buffered")
|
|
self.assertTrue(info["encrypt_at_rest"])
|
|
self.assertEqual(info["flush_interval"], 4.0)
|
|
|
|
def test_active_licensed_features_helper(self):
|
|
"""Tests helper function to extract active licensed capabilities from DB and certificates."""
|
|
Server.init_db(self.test_db)
|
|
Server.SERVER_STATE["ca_cert_pem"] = ""
|
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": "non_existent_test_certs"}
|
|
|
|
# 1. Unlicensed state
|
|
features_unlicensed = Server.get_active_licensed_features(db_path=self.test_db)
|
|
self.assertEqual(features_unlicensed, [])
|
|
self.assertFalse(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
|
|
|
# 2. Add features via JSON string in DB
|
|
conn = sqlite3.connect(self.test_db)
|
|
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db", "ai_heuristics"]),))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
features_licensed = Server.get_active_licensed_features(db_path=self.test_db)
|
|
self.assertEqual(features_licensed, ["ai_heuristics", "enterprise_db"])
|
|
self.assertTrue(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
|
self.assertTrue(Server.is_feature_licensed("ai_heuristics", db_path=self.test_db))
|
|
self.assertFalse(Server.is_feature_licensed("non_existent_feature", db_path=self.test_db))
|
|
|
|
# 3. Verify get_license_summary includes features list
|
|
summary = Server.get_license_summary()
|
|
self.assertIn("features", summary)
|
|
self.assertIn("enterprise_db", summary["features"])
|
|
|
|
def test_webui_tab_rendering_and_feature_cards(self):
|
|
"""Tests WebUI HTML generation for clean tab buttons and feature cards in PKI tab."""
|
|
Server.init_db(self.test_db)
|
|
Server.SERVER_STATE["config"] = {
|
|
"db_path": self.test_db,
|
|
"cert_dir": "certs",
|
|
"tcp_port": 9443,
|
|
"hermes_port": 8443,
|
|
"server_name": "Test-Hub"
|
|
}
|
|
|
|
# 1. Render WebUI in standard / unlicensed mode
|
|
resp = Server.serve_webui(request=None)
|
|
html_content = resp.body.decode("utf-8")
|
|
|
|
# Verify clean tab buttons without number suffixes
|
|
self.assertIn('>💻 Fleet Forwarders</button>', html_content)
|
|
self.assertIn('>🤖 Hermes AI & Tokens</button>', html_content)
|
|
self.assertIn('>📊 Verified Telemetry</button>', html_content)
|
|
self.assertIn('>📜 Sub-CA & Licensing</button>', html_content)
|
|
self.assertIn('>💾 DB Engine</button>', html_content)
|
|
self.assertNotIn('🟢 UNLOCKED', html_content)
|
|
self.assertNotIn('Fleet Forwarders (', html_content)
|
|
|
|
# Verify Licensed Capabilities card is present
|
|
self.assertIn('Licensed Capabilities & Feature Modules', html_content)
|
|
self.assertIn('Standard Community Edition is running', html_content)
|
|
|
|
# 2. Render WebUI with enterprise_db unlocked
|
|
conn = sqlite3.connect(self.test_db)
|
|
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
resp_ent = Server.serve_webui(request=None)
|
|
html_ent = resp_ent.body.decode("utf-8")
|
|
self.assertIn('enterprise_db', html_ent)
|
|
self.assertIn('Enterprise Database & High-Throughput Engine', html_ent)
|
|
self.assertIn('>💾 DB Engine</button>', html_ent)
|
|
self.assertNotIn('🟢 UNLOCKED', html_ent)
|
|
|
|
def test_explicit_opt_in_guarantee(self):
|
|
"""Tests that licensing does NOT automatically turn on encryption or buffering without explicit admin setting."""
|
|
Server.init_db(self.test_db)
|
|
priv_armored, pub_armored, fp = Server.generate_server_keypair("test-opt-in-node")
|
|
priv_key, _ = pgpy.PGPKey.from_blob(priv_armored)
|
|
pub_key, _ = pgpy.PGPKey.from_blob(pub_armored)
|
|
|
|
Server.SERVER_STATE["config"] = {
|
|
"db_path": self.test_db,
|
|
"cert_dir": "certs",
|
|
"storage_engine": "sqlite",
|
|
"cache_mode": "direct",
|
|
"encrypt_at_rest": False,
|
|
"private_key": priv_armored,
|
|
"public_key": pub_armored,
|
|
"server_fingerprint": fp
|
|
}
|
|
Server.SERVER_STATE["private_key_obj"] = priv_key
|
|
Server.SERVER_STATE["public_key_obj"] = pub_key
|
|
|
|
# Unlock enterprise license
|
|
conn = sqlite3.connect(self.test_db)
|
|
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# License is active, but config has encrypt_at_rest=False -> plaintext storage
|
|
raw_msg = "Sample operational log event"
|
|
result_msg = Server.encrypt_field_for_storage(raw_msg)
|
|
self.assertEqual(result_msg, raw_msg)
|
|
self.assertFalse(result_msg.startswith("-----BEGIN PGP MESSAGE-----"))
|
|
|
|
# Ingestion cache mode is direct
|
|
self.assertEqual(Server.SERVER_STATE["config"]["cache_mode"], "direct")
|
|
|
|
def test_reset_enrollment_secret_and_invalidation(self):
|
|
"""Tests that rotating enrollment secret invalidates the previous secret and updates DB & config."""
|
|
init_sec = "initial-secret-123456"
|
|
Server.init_db(self.test_db, enrollment_secret=init_sec, max_seats=5)
|
|
Server.SERVER_STATE["config"] = {
|
|
"db_path": self.test_db,
|
|
"enrollment_secret": init_sec,
|
|
"config_file_path": self.test_config
|
|
}
|
|
with open(self.test_config, "w", encoding="utf-8") as f:
|
|
json.dump({"enrollment_secret": init_sec, "db_path": self.test_db}, f)
|
|
|
|
# 1. Rotate enrollment secret
|
|
new_sec = Server.rotate_enrollment_secret()
|
|
self.assertNotEqual(new_sec, init_sec)
|
|
self.assertEqual(Server.SERVER_STATE["config"]["enrollment_secret"], new_sec)
|
|
|
|
# 2. Check DB
|
|
conn = sqlite3.connect(self.test_db)
|
|
c = conn.cursor()
|
|
c.execute("SELECT enrollment_secret FROM license_config WHERE id = 1")
|
|
db_sec = c.fetchone()[0]
|
|
conn.close()
|
|
self.assertEqual(db_sec, new_sec)
|
|
|
|
# 3. Check config file
|
|
with open(self.test_config, "r", encoding="utf-8") as f:
|
|
cfg_file = json.load(f)
|
|
self.assertEqual(cfg_file["enrollment_secret"], new_sec)
|
|
|
|
# 4. Check export_client_profile automatically generates a new secret
|
|
resp = Server.export_client_profile(server_host="10.0.0.5", server_port=9443)
|
|
exported_profile = json.loads(resp.body.decode("utf-8"))
|
|
self.assertIn("enrollment_secret", exported_profile)
|
|
self.assertNotEqual(exported_profile["enrollment_secret"], new_sec)
|
|
self.assertEqual(exported_profile["enrollment_secret"], Server.SERVER_STATE["config"]["enrollment_secret"])
|
|
|
|
# 5. Check reset API endpoint
|
|
reset_res = Server.reset_server_enrollment_secret()
|
|
self.assertEqual(reset_res["status"], "success")
|
|
self.assertIn("enrollment_secret", reset_res)
|
|
self.assertEqual(reset_res["enrollment_secret"], Server.SERVER_STATE["config"]["enrollment_secret"])
|
|
|
|
def test_hermes_offline_node_detection_and_metrics(self):
|
|
"""Tests that Hermes reporting detects silent forwarders (>15m) and includes host telemetry metrics."""
|
|
Server.init_db(self.test_db)
|
|
Server.SERVER_STATE["config"] = {
|
|
"db_path": self.test_db,
|
|
"evaluation_window_hours": 12
|
|
}
|
|
conn = sqlite3.connect(self.test_db)
|
|
now = datetime.now(timezone.utc)
|
|
thirty_min_ago = (now - timedelta(minutes=30)).strftime("%Y-%m-%d %H:%M:%S")
|
|
two_min_ago = (now - timedelta(minutes=2)).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
metrics_node1 = json.dumps({"cpu_percent": 15.5, "memory_percent": 60.0, "disk_percent_free": 45.0})
|
|
metrics_node2 = json.dumps({"cpu_percent": 88.0, "memory_percent": 95.0, "disk_percent_free": 4.5})
|
|
|
|
conn.execute("""
|
|
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen, metrics_json, cpu_percent, memory_percent, disk_percent_free)
|
|
VALUES ('node-online', 'srv-online.corp', 'windows', 'FP1', 'active', ?, ?, 15.5, 60.0, 45.0)
|
|
""", (two_min_ago, metrics_node1))
|
|
|
|
conn.execute("""
|
|
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, last_seen, metrics_json, cpu_percent, memory_percent, disk_percent_free)
|
|
VALUES ('node-dead', 'srv-dead.corp', 'linux', 'FP2', 'active', ?, ?, 88.0, 95.0, 4.5)
|
|
""", (thirty_min_ago, metrics_node2))
|
|
|
|
# Also insert a verified operational error for the online node
|
|
conn.execute("""
|
|
INSERT INTO active_issues (fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status, last_run_id)
|
|
VALUES ('default:srv-online.corp:AppCrash', 'default', 'srv-online.corp', 'AppCrash', 'ERROR', 'Segmentation fault', 'windows', ?, ?, 2, 'VERIFIED', 'run-1')
|
|
""", (two_min_ago, two_min_ago))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
report = Server.get_verified_anomalies_for_hermes()
|
|
self.assertGreaterEqual(len(report), 2)
|
|
|
|
# Verify offline node alert
|
|
offline_alert = next((r for r in report if r.get("status") == "NODE_OFFLINE"), None)
|
|
self.assertIsNotNone(offline_alert)
|
|
self.assertEqual(offline_alert["server"], "srv-dead.corp")
|
|
self.assertEqual(offline_alert["severity"], "CRITICAL")
|
|
self.assertIn("has gone silent", offline_alert["message"])
|
|
self.assertEqual(offline_alert["metrics"]["disk_percent_free"], 4.5)
|
|
|
|
# Verify online error has attached telemetry
|
|
online_err = next((r for r in report if r.get("signature") == "AppCrash"), None)
|
|
self.assertIsNotNone(online_err)
|
|
self.assertEqual(online_err["metrics"]["cpu_percent"], 15.5)
|
|
|
|
def test_routine_service_noise_filtering(self):
|
|
"""Tests that benign routine background service start/stop cycles are filtered from anomaly spam."""
|
|
self.assertTrue(Server.is_routine_service_noise("sppsvc", "The Software Protection service has entered the stopped state.", "INFO"))
|
|
self.assertTrue(Server.is_routine_service_noise("edgeupdate", "The Microsoft Edge Update service entered the running state.", "INFO"))
|
|
self.assertTrue(Server.is_routine_service_noise("Service Control Manager", "The Windows Update service entered the stopped state.", "INFO"))
|
|
self.assertTrue(Server.is_routine_service_noise("systemd", "Service started successfully.", "INFO"))
|
|
|
|
# Errors or warnings must NOT be filtered
|
|
self.assertFalse(Server.is_routine_service_noise("sppsvc", "Software Protection activation failed with error 0xC004F074", "ERROR"))
|
|
self.assertFalse(Server.is_routine_service_noise("Database", "Connection pool exhausted", "WARNING"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
|
|
|
|
|