Add Windows Server vs Client differentiation, installed server roles introspection, dedicated /client-info directory, and best practice baseline analyzer
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m50s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m50s
This commit is contained in:
+272
-25
@@ -102,6 +102,22 @@ from src.license_manager import LicenseManager
|
||||
from src.pki_manager import PKIManager
|
||||
from src.web_ui import WebUIRenderer, format_relative_time
|
||||
|
||||
try:
|
||||
from src.common.system_info import get_full_system_inventory, get_os_details
|
||||
except ImportError:
|
||||
try:
|
||||
from common.system_info import get_full_system_inventory, get_os_details
|
||||
except ImportError:
|
||||
from system_info import get_full_system_inventory, get_os_details
|
||||
|
||||
try:
|
||||
from src.hub.best_practices import BEST_PRACTICES_ENGINE
|
||||
except ImportError:
|
||||
try:
|
||||
from best_practices import BEST_PRACTICES_ENGINE
|
||||
except ImportError:
|
||||
BEST_PRACTICES_ENGINE = None
|
||||
|
||||
|
||||
CONFIG_FILE_NAME = "server_config.json"
|
||||
DEFAULT_DB_FILE = "logar_state.db"
|
||||
@@ -939,9 +955,15 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
|
||||
|
||||
processed_count += 1
|
||||
|
||||
# Update client node metrics & version if attached
|
||||
# Update client node metrics, version & server role introspection if attached
|
||||
metrics = payload.get("metrics")
|
||||
client_ver = payload.get("version")
|
||||
payload_ost = payload.get("os_type")
|
||||
payload_os_name = payload.get("os_name")
|
||||
payload_is_srv = payload.get("is_server")
|
||||
payload_roles = payload.get("server_roles")
|
||||
payload_info = payload.get("client_info")
|
||||
|
||||
if metrics and isinstance(metrics, dict) and client_server:
|
||||
cpu_p = float(metrics.get("cpu_percent", 0.0))
|
||||
mem_p = float(metrics.get("memory_percent", 0.0))
|
||||
@@ -969,6 +991,24 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Update OS details, server roles and client info telemetry if provided
|
||||
if (payload_os_name or payload_roles is not None or payload_info is not None or payload_is_srv is not None or payload_ost) and client_server:
|
||||
roles_val = json.dumps(payload_roles) if isinstance(payload_roles, list) else (str(payload_roles) if payload_roles is not None else None)
|
||||
info_val = json.dumps(payload_info) if isinstance(payload_info, dict) else (str(payload_info) if payload_info is not None else None)
|
||||
is_srv_val = 1 if payload_is_srv else (0 if payload_is_srv is not None else (1 if payload_ost == "windows_server" else None))
|
||||
try:
|
||||
cursor.execute("""
|
||||
UPDATE clients
|
||||
SET os_type = COALESCE(?, os_type),
|
||||
os_name = COALESCE(NULLIF(?, ''), os_name),
|
||||
is_server = COALESCE(?, is_server),
|
||||
server_roles = COALESCE(?, server_roles),
|
||||
client_info_json = COALESCE(?, client_info_json)
|
||||
WHERE client_id = ? OR hostname = ?
|
||||
""", (payload_ost, payload_os_name, is_srv_val, roles_val, info_val, client_server, client_server))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -997,8 +1037,17 @@ def get_local_system_logs(hours: int = 24, state: Optional[dict] = None) -> list
|
||||
|
||||
def run_local_log_collection_cycle(db_path: str, window_hours: int = 12, min_runs: int = 1, state: Optional[dict] = None) -> int:
|
||||
"""Executes one cycle of local host log collection and direct ingestion into SQLite."""
|
||||
try:
|
||||
inv = get_full_system_inventory() if "get_full_system_inventory" in globals() else {}
|
||||
except Exception:
|
||||
inv = {}
|
||||
|
||||
machine_id = get_machine_identifier()
|
||||
os_type = "windows" if sys.platform == "win32" else "linux"
|
||||
os_type = inv.get("os_type", "windows" if sys.platform == "win32" else "linux")
|
||||
os_name = inv.get("os_name", "")
|
||||
is_server = 1 if inv.get("is_server") else (1 if os_type == "windows_server" else 0)
|
||||
server_roles = json.dumps(inv.get("server_roles", []))
|
||||
client_info_json = json.dumps(inv)
|
||||
now_sql = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
try:
|
||||
@@ -1006,16 +1055,30 @@ def run_local_log_collection_cycle(db_path: str, window_hours: int = 12, min_run
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT client_id FROM clients WHERE client_id = ?", (machine_id,))
|
||||
if c.fetchone():
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = 'LOCAL-HOST-BUILTIN', status = 'active', last_seen = ?
|
||||
WHERE client_id = ?
|
||||
""", (machine_id, os_type, now_sql, machine_id))
|
||||
try:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = 'LOCAL-HOST-BUILTIN', status = 'active', last_seen = ?,
|
||||
os_name = ?, is_server = ?, server_roles = ?, client_info_json = ?
|
||||
WHERE client_id = ?
|
||||
""", (machine_id, os_type, now_sql, os_name, is_server, server_roles, client_info_json, machine_id))
|
||||
except sqlite3.OperationalError:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = 'LOCAL-HOST-BUILTIN', status = 'active', last_seen = ?
|
||||
WHERE client_id = ?
|
||||
""", (machine_id, os_type, now_sql, machine_id))
|
||||
else:
|
||||
c.execute("""
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen)
|
||||
VALUES (?, ?, ?, 'LOCAL-HOST-BUILTIN', 'active', ?, ?)
|
||||
""", (machine_id, machine_id, os_type, now_sql, now_sql))
|
||||
try:
|
||||
c.execute("""
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, os_name, is_server, server_roles, client_info_json)
|
||||
VALUES (?, ?, ?, 'LOCAL-HOST-BUILTIN', 'active', ?, ?, ?, ?, ?, ?)
|
||||
""", (machine_id, machine_id, os_type, now_sql, now_sql, os_name, is_server, server_roles, client_info_json))
|
||||
except sqlite3.OperationalError:
|
||||
c.execute("""
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen)
|
||||
VALUES (?, ?, ?, 'LOCAL-HOST-BUILTIN', 'active', ?, ?)
|
||||
""", (machine_id, machine_id, os_type, now_sql, now_sql))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
@@ -1394,30 +1457,49 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
|
||||
|
||||
now_sql = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
client_ver = req.version or ""
|
||||
req_os_name = req.os_name or ""
|
||||
req_is_server = 1 if req.is_server else (1 if req.os == "windows_server" else 0)
|
||||
req_server_roles = json.dumps(req.server_roles) if req.server_roles else "[]"
|
||||
req_client_info = json.dumps(req.client_info) if req.client_info else "{}"
|
||||
|
||||
if existing:
|
||||
try:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = ?, status = 'active', last_seen = ?, client_version = ?
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = ?, status = 'active', last_seen = ?, client_version = ?,
|
||||
os_name = ?, is_server = ?, server_roles = ?, client_info_json = ?
|
||||
WHERE client_id = ?
|
||||
""", (req.hostname, req.os, fp, now_sql, client_ver, req.client_id))
|
||||
""", (req.hostname, req.os, fp, now_sql, client_ver, req_os_name, req_is_server, req_server_roles, req_client_info, req.client_id))
|
||||
except sqlite3.OperationalError:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = ?, status = 'active', last_seen = ?
|
||||
WHERE client_id = ?
|
||||
""", (req.hostname, req.os, fp, now_sql, req.client_id))
|
||||
try:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = ?, status = 'active', last_seen = ?, client_version = ?
|
||||
WHERE client_id = ?
|
||||
""", (req.hostname, req.os, fp, now_sql, client_ver, req.client_id))
|
||||
except sqlite3.OperationalError:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
SET hostname = ?, os_type = ?, cert_fingerprint = ?, status = 'active', last_seen = ?
|
||||
WHERE client_id = ?
|
||||
""", (req.hostname, req.os, fp, now_sql, req.client_id))
|
||||
else:
|
||||
try:
|
||||
c.execute("""
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, client_version)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?, ?)
|
||||
""", (req.client_id, req.hostname, req.os, fp, now_sql, now_sql, client_ver))
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, client_version, os_name, is_server, server_roles, client_info_json)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (req.client_id, req.hostname, req.os, fp, now_sql, now_sql, client_ver, req_os_name, req_is_server, req_server_roles, req_client_info))
|
||||
except sqlite3.OperationalError:
|
||||
c.execute("""
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?)
|
||||
""", (req.client_id, req.hostname, req.os, fp, now_sql, now_sql))
|
||||
try:
|
||||
c.execute("""
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, client_version)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?, ?)
|
||||
""", (req.client_id, req.hostname, req.os, fp, now_sql, now_sql, client_ver))
|
||||
except sqlite3.OperationalError:
|
||||
c.execute("""
|
||||
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?)
|
||||
""", (req.client_id, req.hostname, req.os, fp, now_sql, now_sql))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -2484,6 +2566,171 @@ def serve_webui(request: Request = None):
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
|
||||
@app.get("/client-info", response_class=HTMLResponse)
|
||||
@app.get("/webui/client-info", response_class=HTMLResponse)
|
||||
def serve_client_info(request: Request = None):
|
||||
"""Renders the dedicated LOGAR Client & Server Role Directory subpage."""
|
||||
auth_user = None
|
||||
if request is not None:
|
||||
auth_user = get_current_user_optional(request)
|
||||
if not auth_user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
else:
|
||||
auth_user = DEFAULT_ADMIN_USER
|
||||
|
||||
html_content = WebUIRenderer.render_client_info_html(
|
||||
auth_user=auth_user,
|
||||
request=request,
|
||||
server_state=SERVER_STATE
|
||||
)
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
|
||||
@app.get("/api/client-info")
|
||||
def api_client_info(request: Request = None, auth_user: str = Depends(require_admin_auth)):
|
||||
"""API endpoint providing comprehensive host telemetry, OS classification, and server roles."""
|
||||
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("""
|
||||
SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen,
|
||||
COALESCE(client_version, ''), COALESCE(os_name, ''), COALESCE(is_server, 0),
|
||||
COALESCE(server_roles, '[]'), COALESCE(client_info_json, '{}'),
|
||||
COALESCE(cpu_percent, 0.0), COALESCE(memory_percent, 0.0), COALESCE(disk_percent_free, 0.0)
|
||||
FROM clients
|
||||
ORDER BY is_server DESC, last_seen DESC
|
||||
""")
|
||||
rows = c.fetchall()
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
raise HTTPException(status_code=500, detail=f"Database query error: {str(e)}")
|
||||
conn.close()
|
||||
|
||||
clients_list = []
|
||||
win_servers = 0
|
||||
win_workstations = 0
|
||||
linux_nodes = 0
|
||||
|
||||
for r in rows:
|
||||
cid, host, ost, fp, st, first, last, cver, os_name, is_srv, roles_raw, info_raw, cpu_p, mem_p, disk_p = r
|
||||
try:
|
||||
roles = json.loads(roles_raw) if roles_raw else []
|
||||
except Exception:
|
||||
roles = []
|
||||
try:
|
||||
info = json.loads(info_raw) if info_raw else {}
|
||||
except Exception:
|
||||
info = {}
|
||||
|
||||
is_srv_bool = bool(is_srv) or ost == "windows_server"
|
||||
if "server" in ost or is_srv_bool:
|
||||
if "windows" in ost:
|
||||
win_servers += 1
|
||||
else:
|
||||
linux_nodes += 1
|
||||
elif "windows" in ost:
|
||||
win_workstations += 1
|
||||
elif "linux" in ost:
|
||||
linux_nodes += 1
|
||||
|
||||
clients_list.append({
|
||||
"client_id": cid,
|
||||
"hostname": host,
|
||||
"os_type": ost,
|
||||
"os_name": os_name,
|
||||
"is_server": is_srv_bool,
|
||||
"server_roles": roles,
|
||||
"client_info": info,
|
||||
"cert_fingerprint": fp,
|
||||
"status": st,
|
||||
"first_seen": first,
|
||||
"last_seen": last,
|
||||
"client_version": cver,
|
||||
"metrics": {
|
||||
"cpu_percent": cpu_p,
|
||||
"memory_percent": mem_p,
|
||||
"disk_percent_free": disk_p
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"total_nodes": len(clients_list),
|
||||
"windows_servers": win_servers,
|
||||
"windows_workstations": win_workstations,
|
||||
"linux_nodes": linux_nodes,
|
||||
"clients": clients_list
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/best-practices/status")
|
||||
def api_best_practices_status(request: Request = None, auth_user: str = Depends(require_admin_auth)):
|
||||
"""API endpoint providing fleet-wide Best Practice baseline compliance evaluation."""
|
||||
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||
is_licensed = is_feature_licensed("best_practices", db_path)
|
||||
|
||||
if not is_licensed:
|
||||
return {
|
||||
"status": "locked",
|
||||
"licensed": False,
|
||||
"message": "The Best Practice Analyzer is an enterprise capability that requires an active license with the 'best_practices' feature enabled.",
|
||||
"rules_available": len(BEST_PRACTICES_ENGINE.get_rules()) if BEST_PRACTICES_ENGINE else 0
|
||||
}
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("""
|
||||
SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen,
|
||||
COALESCE(client_version, ''), COALESCE(os_name, ''), COALESCE(is_server, 0),
|
||||
COALESCE(server_roles, '[]'), COALESCE(client_info_json, '{}'),
|
||||
COALESCE(cpu_percent, 0.0), COALESCE(memory_percent, 0.0), COALESCE(disk_percent_free, 0.0),
|
||||
COALESCE(update_status, 'up_to_date')
|
||||
FROM clients
|
||||
WHERE status = 'active'
|
||||
""")
|
||||
rows = c.fetchall()
|
||||
except Exception:
|
||||
rows = []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
node_records = []
|
||||
for r in rows:
|
||||
cid, host, ost, fp, st, first, last, cver, os_name, is_srv, roles_raw, info_raw, cpu_p, mem_p, disk_p, up_st = r
|
||||
try:
|
||||
roles = json.loads(roles_raw) if roles_raw else []
|
||||
except Exception:
|
||||
roles = []
|
||||
try:
|
||||
info = json.loads(info_raw) if info_raw else {}
|
||||
except Exception:
|
||||
info = {}
|
||||
|
||||
node_records.append({
|
||||
"client_id": cid,
|
||||
"hostname": host,
|
||||
"os_type": ost,
|
||||
"os_name": os_name,
|
||||
"is_server": bool(is_srv) or ost == "windows_server",
|
||||
"server_roles": roles,
|
||||
"client_info": info,
|
||||
"cpu_percent": cpu_p,
|
||||
"memory_percent": mem_p,
|
||||
"disk_percent_free": disk_p,
|
||||
"client_version": cver,
|
||||
"update_status": up_st
|
||||
})
|
||||
|
||||
eval_result = BEST_PRACTICES_ENGINE.evaluate_fleet(node_records) if BEST_PRACTICES_ENGINE else {}
|
||||
return {
|
||||
"status": "active",
|
||||
"licensed": True,
|
||||
"evaluation": eval_result
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user