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

This commit is contained in:
2026-10-11 09:37:05 +02:00
parent bcaae4a9ee
commit cccf928f29
11 changed files with 1902 additions and 38 deletions
+278
View File
@@ -0,0 +1,278 @@
"""
LOGAR Best Practice Analyzer & Security Baseline Engine
Provides a modular, extensible framework for verifying server configuration baselines,
role-specific best practices, and security hardening across enrolled fleet nodes.
Gated by the 'best_practices' enterprise license capability.
"""
import json
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional, Callable
class BestPracticeRule:
"""Represents a single modular Best Practice configuration check."""
def __init__(
self,
rule_id: str,
category: str,
title: str,
description: str,
recommendation: str,
severity: str = "WARNING", # "CRITICAL" | "WARNING" | "RECOMMENDED"
target_role: Optional[str] = None,
target_os: Optional[str] = None,
eval_fn: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None
):
self.rule_id = rule_id
self.category = category
self.title = title
self.description = description
self.recommendation = recommendation
self.severity = severity
self.target_role = target_role
self.target_os = target_os
self.eval_fn = eval_fn
def to_dict(self) -> Dict[str, Any]:
return {
"rule_id": self.rule_id,
"category": self.category,
"title": self.title,
"description": self.description,
"recommendation": self.recommendation,
"severity": self.severity,
"target_role": self.target_role,
"target_os": self.target_os
}
class BestPracticeEngine:
"""Evaluates rules across enrolled server nodes and collects compliance telemetry."""
def __init__(self):
self._rules: Dict[str, BestPracticeRule] = {}
self._load_baseline_rules()
def register_rule(self, rule: BestPracticeRule):
"""Registers a custom Best Practice check. Enables users to define rules later on."""
self._rules[rule.rule_id] = rule
def get_rules(self) -> List[Dict[str, Any]]:
return [r.to_dict() for r in self._rules.values()]
def _load_baseline_rules(self):
# 1. Disk Capacity Health
def eval_disk_space(node: Dict[str, Any]) -> Dict[str, Any]:
pct = float(node.get("disk_percent_free") or 0.0)
if pct <= 0.0:
return {"status": "NOT_APPLICABLE", "message": "Disk capacity metrics pending telemetry collection."}
if pct < 10.0:
return {"status": "FAIL", "message": f"Critical disk headroom: only {pct}% free capacity remaining on primary volume."}
if pct < 20.0:
return {"status": "WARN", "message": f"Low disk headroom: {pct}% free capacity remaining (recommended >= 20%)."}
return {"status": "PASS", "message": f"Healthy disk capacity: {pct}% free capacity available."}
self.register_rule(BestPracticeRule(
rule_id="BP-SYS-001",
category="System Health",
title="System Volume Storage Headroom (> 20% Free)",
description="Verifies that server and workstation system drives retain at least 20% free space to ensure unhindered OS paging, log buffering, and update installation.",
recommendation="Expand volume storage or clear transient files to maintain healthy write headroom.",
severity="CRITICAL",
eval_fn=eval_disk_space
))
# 2. Memory Headroom
def eval_memory_load(node: Dict[str, Any]) -> Dict[str, Any]:
pct = float(node.get("memory_percent") or 0.0)
if pct <= 0.0:
return {"status": "NOT_APPLICABLE", "message": "Memory metrics pending telemetry collection."}
if pct >= 95.0:
return {"status": "FAIL", "message": f"Exhausted memory headroom: {pct}% active memory consumption."}
if pct >= 88.0:
return {"status": "WARN", "message": f"Elevated memory load: {pct}% utilization (recommended < 85%)."}
return {"status": "PASS", "message": f"Memory utilization optimal: {pct}% in use."}
self.register_rule(BestPracticeRule(
rule_id="BP-SYS-002",
category="System Health",
title="Memory Saturation & Buffer Safety (< 85% Load)",
description="Ensures system RAM utilization remains below critical saturation thresholds to prevent kernel thread swapping and out-of-memory service crashes.",
recommendation="Review high-consumption service processes or allocate additional physical RAM to this server.",
severity="WARNING",
eval_fn=eval_memory_load
))
# 3. Server Role Segregation
def eval_dc_co_location(node: Dict[str, Any]) -> Dict[str, Any]:
roles = node.get("server_roles") or []
if isinstance(roles, str):
try:
roles = json.loads(roles)
except Exception:
roles = [r.strip() for r in roles.split(",") if r.strip()]
roles_lower = [str(r).lower() for r in roles]
has_ad = any("active directory" in r for r in roles_lower)
if not has_ad:
return {"status": "NOT_APPLICABLE", "message": "Node is not designated as an Active Directory Domain Controller."}
# Check for high-risk co-located roles on Domain Controllers
conflicts = []
if any("web server" in r or "iis" in r for r in roles_lower):
conflicts.append("Web Server (IIS)")
if any("remote desktop" in r or "terminal" in r for r in roles_lower):
conflicts.append("Remote Desktop Services")
if any("print" in r for r in roles_lower):
conflicts.append("Print & Document Services")
if conflicts:
return {"status": "FAIL", "message": f"Active Directory Domain Controller co-locates non-recommended roles: {', '.join(conflicts)}."}
return {"status": "PASS", "message": "Domain Controller adheres to dedicated role isolation standards."}
self.register_rule(BestPracticeRule(
rule_id="BP-AD-001",
category="Active Directory",
title="Domain Controller Role Isolation Baseline",
description="Domain Controllers should be dedicated solely to identity services. Co-locating IIS, Remote Desktop, or Print Spooler introduces lateral privilege escalation attack vectors.",
recommendation="Migrate auxiliary web and print services to separate member servers.",
severity="CRITICAL",
target_role="Active Directory Domain Services",
target_os="windows_server",
eval_fn=eval_dc_co_location
))
# 4. Windows Server Detection and Role Discovery
def eval_server_classification(node: Dict[str, Any]) -> Dict[str, Any]:
is_server = bool(node.get("is_server"))
os_type = str(node.get("os_type", "")).lower()
os_name = str(node.get("os_name", ""))
roles = node.get("server_roles") or []
if isinstance(roles, str):
try:
roles = json.loads(roles)
except Exception:
roles = []
if "server" in os_type or "server" in os_name.lower():
if len(roles) > 0:
return {"status": "PASS", "message": f"Windows Server edition identified ({os_name}) with {len(roles)} detected enterprise roles."}
else:
return {"status": "WARN", "message": f"Windows Server identified ({os_name}), but no server roles currently indexed."}
elif os_type in ("windows", "workstation"):
return {"status": "PASS", "message": f"Workstation node ({os_name}) correctly classified without server role footprint."}
return {"status": "PASS", "message": f"Host classification verified: {os_name or os_type}."}
self.register_rule(BestPracticeRule(
rule_id="BP-WIN-002",
category="Role Baseline",
title="Operating System Edition & Server Role Alignment",
description="Verifies that nodes are accurately differentiated between client workstations and enterprise servers, and that active server roles are fully inventoried.",
recommendation="Ensure the LOGAR edge forwarder runs with sufficient permissions to query Windows Server Manager.",
severity="RECOMMENDED",
eval_fn=eval_server_classification
))
# 5. Forwarder Agent Freshness
def eval_agent_version(node: Dict[str, Any]) -> Dict[str, Any]:
ver = str(node.get("client_version") or "").strip()
up_status = str(node.get("update_status") or "up_to_date").lower()
if not ver:
return {"status": "WARN", "message": "Client version not yet reported to hub."}
if up_status == "outdated":
return {"status": "WARN", "message": f"Edge client version (v{ver}) is outdated relative to hosted hub binaries."}
return {"status": "PASS", "message": f"Edge forwarder binary (v{ver}) is current."}
self.register_rule(BestPracticeRule(
rule_id="BP-SEC-003",
category="Security Baseline",
title="Edge Forwarder Binary Version Baseline",
description="Ensures forwarder nodes run the latest patched agent binary with current TLS 1.3 cryptographic cipher suites and security fixes.",
recommendation="Enable Fleet Auto-Update or update the forwarder executable to the latest hosted release.",
severity="RECOMMENDED",
eval_fn=eval_agent_version
))
def evaluate_node(self, node: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Evaluates all applicable rules against a single server/node record."""
results = []
for rule in self._rules.values():
# Check target OS constraint
if rule.target_os and str(node.get("os_type", "")).lower() != rule.target_os.lower():
continue
# Check target role constraint
if rule.target_role:
roles = node.get("server_roles") or []
if isinstance(roles, str):
try:
roles = json.loads(roles)
except Exception:
roles = [r.strip() for r in roles.split(",") if r.strip()]
roles_str = " ".join(str(r) for r in roles).lower()
if rule.target_role.lower() not in roles_str:
continue
eval_res = {"status": "PASS", "message": "Check passed baseline standards."}
if rule.eval_fn:
try:
eval_res = rule.eval_fn(node)
except Exception as ex:
eval_res = {"status": "ERROR", "message": f"Evaluation error: {str(ex)}"}
results.append({
"rule_id": rule.rule_id,
"category": rule.category,
"title": rule.title,
"severity": rule.severity,
"recommendation": rule.recommendation,
"status": eval_res.get("status", "PASS"),
"message": eval_res.get("message", "")
})
return results
def evaluate_fleet(self, nodes: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Evaluates all registered rules across all fleet nodes and returns aggregated summary."""
fleet_results = {}
pass_count = 0
warn_count = 0
fail_count = 0
for node in nodes:
cid = node.get("client_id") or node.get("hostname") or "unknown"
node_checks = self.evaluate_node(node)
fleet_results[cid] = {
"hostname": node.get("hostname", cid),
"os_type": node.get("os_type", "unknown"),
"os_name": node.get("os_name", ""),
"is_server": bool(node.get("is_server")),
"checks": node_checks
}
for chk in node_checks:
st = chk.get("status")
if st == "PASS":
pass_count += 1
elif st == "WARN":
warn_count += 1
elif st == "FAIL":
fail_count += 1
total_evaluated = pass_count + warn_count + fail_count
score = int((pass_count / total_evaluated * 100)) if total_evaluated > 0 else 100
return {
"score": score,
"pass_count": pass_count,
"warn_count": warn_count,
"fail_count": fail_count,
"total_evaluated": total_evaluated,
"evaluated_nodes_count": len(nodes),
"fleet_results": fleet_results,
"rules_count": len(self._rules)
}
# Global singleton instance
BEST_PRACTICES_ENGINE = BestPracticeEngine()