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
+7
View File
@@ -1 +1,8 @@
# LOGAR Common Package
from src.common.system_info import (
get_os_details,
get_installed_server_roles,
get_full_system_inventory,
get_ip_addresses
)
from src.common.version import LOGAR_VERSION
+323
View File
@@ -0,0 +1,323 @@
"""
LOGAR System & Server Role Introspection Module
Provides cross-platform OS classification (Windows Client vs Windows Server vs Linux),
installed Server Role detection, and comprehensive host inventory telemetry.
"""
import os
import sys
import json
import socket
import shutil
import platform
import subprocess
from typing import Dict, Any, List, Optional
def get_ip_addresses() -> List[str]:
"""Retrieves all non-loopback IPv4 addresses assigned to the local host."""
ips = set()
try:
hostname = socket.gethostname()
for info in socket.getaddrinfo(hostname, None, socket.AF_INET):
ip = info[4][0]
if ip and not ip.startswith("127."):
ips.add(ip)
except Exception:
pass
try:
# Fallback via outbound UDP socket probe (doesn't send traffic)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0.1)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
if local_ip and not local_ip.startswith("127."):
ips.add(local_ip)
except Exception:
pass
return sorted(list(ips))
def get_os_details() -> Dict[str, Any]:
"""
Accurately classifies the operating system, explicitly differentiating between
Windows Client (Workstation) and Windows Server, as well as Linux distributions.
Returns:
{
"os_type": "windows_server" | "windows" | "linux" | "darwin" | "unknown",
"os_name": str, # e.g. "Windows Server 2022 Datacenter", "Windows 11 Pro", "Ubuntu 22.04 LTS"
"is_server": bool, # True for Windows Server or Linux Server; False for desktop clients
"edition": str, # e.g. "ServerDatacenter", "Professional", "Enterprise"
"build": str, # OS build number
"architecture": str, # "AMD64", "x86_64", "ARM64"
"hostname": str,
"ip_addresses": List[str]
}
"""
is_server = False
os_type = "unknown"
os_name = platform.platform()
edition = ""
build = platform.version()
arch = platform.machine() or ("64-bit" if sys.maxsize > 2**32 else "32-bit")
if sys.platform == "win32":
try:
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion") as key:
pname = str(winreg.QueryValueEx(key, "ProductName")[0])
try:
itype = str(winreg.QueryValueEx(key, "InstallationType")[0])
except Exception:
itype = ""
try:
edition = str(winreg.QueryValueEx(key, "EditionID")[0])
except Exception:
pass
try:
build = str(winreg.QueryValueEx(key, "CurrentBuild")[0])
except Exception:
pass
os_name = pname
# Check for Windows Server installation type or product name
if itype.strip().lower() == "server" or "server" in pname.lower():
is_server = True
os_type = "windows_server"
else:
is_server = False
os_type = "windows"
except Exception:
# Fallback for Windows
win_ver = platform.win32_ver()
os_name = f"Windows {win_ver[0]}"
if "server" in os_name.lower():
is_server = True
os_type = "windows_server"
else:
is_server = False
os_type = "windows"
elif sys.platform.startswith("linux"):
os_type = "linux"
is_server = True # Linux in enterprise log forwarding contexts is typically server/node
if os.path.exists("/etc/os-release"):
try:
with open("/etc/os-release", "r", encoding="utf-8") as f:
for line in f:
if line.startswith("PRETTY_NAME="):
os_name = line.strip().split("=", 1)[1].strip('"\'')
break
except Exception:
pass
elif sys.platform == "darwin":
os_type = "darwin"
is_server = False
os_name = f"macOS {platform.mac_ver()[0]}"
else:
os_type = sys.platform
is_server = False
return {
"os_type": os_type,
"os_name": os_name,
"is_server": is_server,
"edition": edition,
"build": build,
"architecture": arch,
"hostname": socket.gethostname(),
"ip_addresses": get_ip_addresses()
}
def get_installed_windows_server_roles() -> List[str]:
"""
Detects installed Windows Server roles and major features.
Combines authoritative PowerShell ServerManager cmdlets on Windows Server with
resilient service & registry checks.
"""
if sys.platform != "win32":
return []
roles = set()
# 1. Authoritative PowerShell query on Windows Server
try:
ps_cmd = (
"try { "
"Get-WindowsFeature | Where-Object { $_.Installed -eq $true -and ($_.FeatureType -eq 'Role' -or ($_.Parent -eq '' -and $_.Installed -eq $true)) } | Select-Object -ExpandProperty DisplayName "
"} catch { }"
)
proc = subprocess.run(
["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd],
capture_output=True,
text=True,
timeout=6
)
if proc.returncode == 0 and proc.stdout:
for line in proc.stdout.splitlines():
role_name = line.strip()
if role_name and len(role_name) > 2:
roles.add(role_name)
except Exception:
pass
# 2. Service and Registry Inspection (Instant fallback and verification)
ROLE_SERVICE_MAP = {
"NTDS": "Active Directory Domain Services",
"DNS": "DNS Server",
"DHCPServer": "DHCP Server",
"W3SVC": "Web Server (IIS)",
"CertSvc": "Active Directory Certificate Services",
"vmms": "Hyper-V",
"TermService": "Remote Desktop Services",
"LanmanServer": "File and Storage Services",
"WsusService": "Windows Server Update Services (WSUS)",
"ClusSvc": "Failover Clustering",
"RemoteAccess": "Remote Access / DirectAccess",
"ADAM": "Active Directory Lightweight Directory Services",
"adfssrv": "Active Directory Federation Services",
"IAS": "Network Policy Server (NPS)",
"Spooler": "Print and Document Services",
"MSMQ": "Message Queuing (MSMQ)",
}
try:
import winreg
for svc_name, role_title in ROLE_SERVICE_MAP.items():
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, rf"SYSTEM\CurrentControlSet\Services\{svc_name}") as k:
# Key exists; check if service is enabled (Start != 4 disabled)
try:
start_type = winreg.QueryValueEx(k, "Start")[0]
if start_type in (2, 3): # 2=Auto, 3=Manual
# For LanmanServer / Spooler / TermService, only count as dedicated role if on Server
roles.add(role_title)
except Exception:
roles.add(role_title)
except (FileNotFoundError, OSError):
pass
except Exception:
pass
# Clean up and normalize role names
return sorted(list(roles))
def get_installed_linux_server_roles() -> List[str]:
"""Detects installed server roles and major service daemons on Linux hosts."""
if not sys.platform.startswith("linux"):
return []
roles = set()
SERVICE_MAP = {
"named": "DNS Server (BIND)",
"bind9": "DNS Server (BIND)",
"dnsmasq": "DNS & DHCP Server (dnsmasq)",
"isc-dhcp-server": "DHCP Server",
"nginx": "Web Server (Nginx)",
"apache2": "Web Server (Apache)",
"httpd": "Web Server (Apache)",
"sshd": "SSH Remote Access",
"smbd": "File Server (Samba/SMB)",
"nfs-server": "File Server (NFS)",
"docker": "Container Host (Docker)",
"containerd": "Container Runtime (containerd)",
"podman": "Container Engine (Podman)",
"postgresql": "Database Server (PostgreSQL)",
"mariadb": "Database Server (MariaDB)",
"mysql": "Database Server (MySQL)",
"postfix": "Mail Transfer Agent (Postfix)",
"dovecot": "Mail Delivery Agent (Dovecot)",
"haproxy": "Load Balancer (HAProxy)",
"keepalived": "High Availability (Keepalived)",
"ufw": "Host Firewall (UFW)",
"firewalld": "Host Firewall (firewalld)",
}
for svc, title in SERVICE_MAP.items():
if shutil.which(svc):
roles.add(title)
return sorted(list(roles))
def get_installed_server_roles() -> List[str]:
"""Cross-platform dispatcher for installed server roles and features."""
if sys.platform == "win32":
return get_installed_windows_server_roles()
elif sys.platform.startswith("linux"):
return get_installed_linux_server_roles()
return []
def get_full_system_inventory() -> Dict[str, Any]:
"""
Gathers comprehensive hardware, OS, network, and server role inventory.
"""
os_info = get_os_details()
roles = get_installed_server_roles() if os_info.get("is_server") else []
# Disk capacity
disk_total_gb = 0.0
disk_free_gb = 0.0
disk_pct_free = 0.0
try:
path = "C:\\" if sys.platform == "win32" else "/"
total, _, free = shutil.disk_usage(path)
disk_total_gb = round(total / (1024 ** 3), 2)
disk_free_gb = round(free / (1024 ** 3), 2)
disk_pct_free = round((free / total) * 100, 1) if total > 0 else 0.0
except Exception:
pass
# Memory
mem_total_mb = 0
try:
if sys.platform == "win32":
import ctypes
class MEMORYSTATUSEX(ctypes.Structure):
_fields_ = [
("dwLength", ctypes.c_ulong),
("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong),
("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalPageFile", ctypes.c_ulonglong),
("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong),
("ullAvailVirtual", ctypes.c_ulonglong),
("sullAvailExtendedVirtual", ctypes.c_ulonglong),
]
stat = MEMORYSTATUSEX()
stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
mem_total_mb = int(stat.ullTotalPhys / (1024 * 1024))
elif os.path.exists("/proc/meminfo"):
with open("/proc/meminfo", "r", encoding="utf-8") as f:
for line in f:
if line.startswith("MemTotal:"):
mem_total_mb = int(line.split(":")[1].strip().split()[0]) // 1024
break
except Exception:
pass
return {
"hostname": os_info.get("hostname"),
"os_type": os_info.get("os_type"),
"os_name": os_info.get("os_name"),
"is_server": os_info.get("is_server"),
"edition": os_info.get("edition"),
"build": os_info.get("build"),
"architecture": os_info.get("architecture"),
"cpu_count": os.cpu_count() or 1,
"memory_total_mb": mem_total_mb,
"disk_total_gb": disk_total_gb,
"disk_free_gb": disk_free_gb,
"disk_percent_free": disk_pct_free,
"ip_addresses": os_info.get("ip_addresses", []),
"server_roles": roles
}