import os import sys import json import socket import struct import argparse import warnings from datetime import datetime, timezone, timedelta # Suppress cryptography / pgpy deprecation notices warnings.filterwarnings("ignore") import pgpy try: import win32evtlog except ImportError: win32evtlog = None CONFIG_FILE_NAME = "client_config.json" def load_config(config_path: str = CONFIG_FILE_NAME): if not os.path.exists(config_path): raise FileNotFoundError( f"Client configuration file not found at: {config_path}\n" f"Generate one from the server using: python Server.py --create-client-config --client-out {config_path}" ) with open(config_path, "r", encoding="utf-8") as f: return json.load(f) def get_machine_identifier() -> str: """ Returns the hostname of the machine sending the logs, and appends the network/DNS domain if available. """ fqdn = socket.getfqdn() if fqdn and "." in fqdn and not fqdn.startswith("localhost"): return fqdn hostname = socket.gethostname() user_dns_domain = os.environ.get("USERDNSDOMAIN") if user_dns_domain and user_dns_domain.lower() != hostname.lower(): return f"{hostname}.{user_dns_domain.lower()}" try: host_ip = socket.gethostbyname(hostname) canonical_name = socket.gethostbyaddr(host_ip)[0] if canonical_name and "." in canonical_name and not canonical_name.startswith("localhost"): return canonical_name except Exception: pass return hostname def get_recent_windows_logs(hours: int = 6): """ Scans the Windows Application Event Log backwards for events within the window. Edge Thinness & Noise Stripping: INFO and DEBUG events are dropped at the source. """ if win32evtlog is None: print("[!] pywin32 is not installed or not running on Windows. Returning mock/empty candidate list.") return [] server = "localhost" log_type = "Application" flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ try: hand = win32evtlog.OpenEventLog(server, log_type) except Exception as e: print(f"[!] Error opening Windows event log: {e}") return [] logs = [] cutoff_time = datetime.now() - timedelta(hours=hours) machine_id = get_machine_identifier() sev_map = { 1: "CRITICAL", 2: "ERROR", 3: "WARNING" } while True: events = win32evtlog.ReadEventLog(hand, flags, 0) if not events: break for event in events: if event.TimeGenerated < cutoff_time: break # Drop conversational or informational noise (INFO=4, etc.) at source # Only retain Critical (1), Error (2), and Warning (3) if event.EventType in sev_map: msg = " ".join(event.StringInserts) if event.StringInserts else "Event Log Entry" logs.append({ "server": machine_id, "os_type": "windows", "signature": event.SourceName or "Windows-Event", "severity": sev_map[event.EventType], "message": msg[:2048] # Limit message length }) if events[-1].TimeGenerated < cutoff_time: break win32evtlog.CloseEventLog(hand) return logs def send_encrypted_logs_over_socket(config: dict, logs: list): """ Encrypts the payload using the server's OpenPGP public key and streams over an authenticated TCP socket. Zero local state is maintained on the client. """ server_host = config["server_host"] server_port = int(config["server_port"]) auth_token = config["auth_token"] pub_key_armored = config["server_public_key"] expected_fp = config.get("server_fingerprint", "").replace(" ", "").upper() # Load and verify server public key pub_key, _ = pgpy.PGPKey.from_blob(pub_key_armored) actual_fp = str(pub_key.fingerprint).replace(" ", "").upper() if expected_fp and actual_fp != expected_fp: raise ValueError( f"Server fingerprint mismatch! Expected {expected_fp}, but key has {actual_fp}." ) machine_id = get_machine_identifier() # Prepare zero-state candidate batch payload = { "server": machine_id, "timestamp": datetime.now(timezone.utc).isoformat(), "logs": logs } payload_json = json.dumps(payload) # Encrypt payload with server's encryption-only key pgp_msg = pgpy.PGPMessage.new(payload_json) encrypted_msg = pub_key.encrypt(pgp_msg) encrypted_armored = str(encrypted_msg) # Envelope with socket authentication header envelope = { "auth_token": auth_token, "timestamp": datetime.now(timezone.utc).isoformat(), "encrypted_payload": encrypted_armored } envelope_bytes = json.dumps(envelope).encode("utf-8") # Connect over TCP socket and transmit with 4-byte length prefix framing print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over secure TCP socket...") with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(15.0) sock.connect((server_host, server_port)) # Send frame: length (4 bytes big-endian) + envelope frame = struct.pack(">I", len(envelope_bytes)) + envelope_bytes sock.sendall(frame) # Receive response length resp_len_bytes = sock.recv(4) if not resp_len_bytes: raise ConnectionError("Server closed connection without response.") resp_len = struct.unpack(">I", resp_len_bytes)[0] resp_bytes = bytearray() while len(resp_bytes) < resp_len: chunk = sock.recv(min(4096, resp_len - len(resp_bytes))) if not chunk: break resp_bytes.extend(chunk) response = json.loads(resp_bytes.decode("utf-8")) print(f"[+] Server response: {response}") return response def main(): parser = argparse.ArgumentParser(description="LOGAR Windows Edge Log Forwarder (Zero State)") parser.add_argument("--config", default=CONFIG_FILE_NAME, help="Path to client_config.json") parser.add_argument("--hours", type=int, default=6, help="Lookback window in hours for event logs") args = parser.parse_args() try: config = load_config(args.config) except Exception as e: print(f"[!] Configuration error: {e}") sys.exit(1) print(f"[*] Scanning Windows Application event log for candidate anomalies (last {args.hours} hours)...") candidate_logs = get_recent_windows_logs(hours=args.hours) print(f"[*] Found {len(candidate_logs)} candidate anomalies (noise stripped at source).") try: send_encrypted_logs_over_socket(config, candidate_logs) except Exception as e: print(f"[!] Failed to stream logs to server: {e}") sys.exit(1) if __name__ == "__main__": main()