""" LOGAR Windows Edge Log Forwarder Collects Windows Event Logs (Application/System) and securely streams them to the central LOGAR Hub via mutual TLS (mTLS 1.3) or OpenPGP envelope. """ import os import sys import time import argparse import warnings from typing import Optional # Ensure script directory and repo root are in sys.path SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) if SCRIPT_DIR not in sys.path: sys.path.insert(0, SCRIPT_DIR) REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) if REPO_ROOT not in sys.path: sys.path.insert(0, REPO_ROOT) # Suppress cryptography / pgpy deprecation notices warnings.filterwarnings("ignore") import pgpy try: import win32evtlog except ImportError: win32evtlog = None try: from src.clients.log_collector import ( get_machine_identifier, is_path_writable, get_default_data_dir, resolve_writable_path, get_state_path, load_state, save_state, commit_state, WindowsLogCollector, BaseLogCollector ) from src.clients.client_transport import ( is_cert_expiring_soon, enroll_client_if_needed, get_tls_socket, load_config, ensure_enrolled as transport_ensure_enrolled, send_encrypted_logs_over_socket as transport_send_encrypted_logs, check_and_apply_auto_update ) except ImportError: from log_collector import ( get_machine_identifier, is_path_writable, get_default_data_dir, resolve_writable_path, get_state_path, load_state, save_state, commit_state, WindowsLogCollector, BaseLogCollector ) from client_transport import ( is_cert_expiring_soon, enroll_client_if_needed, get_tls_socket, load_config, ensure_enrolled as transport_ensure_enrolled, send_encrypted_logs_over_socket as transport_send_encrypted_logs, check_and_apply_auto_update ) CONFIG_FILE_NAME = "client_config.json" STATE_FILE_NAME = "client_state.json" def get_recent_windows_logs(hours: int = 24, state: Optional[dict] = None) -> list: """ Scans the Windows Application Event Log backwards for events within the window. Edge Filtering: Retains INFO, WARNING, and ERROR. Drops Audit and Debug noise. State Tracking: Skips events older than lookback window and already sent records. """ collector = WindowsLogCollector(state=state, channels=["Application"]) return collector.collect_logs(hours=hours) try: from src.common.system_info import get_os_details except ImportError: try: from common.system_info import get_os_details except ImportError: get_os_details = lambda: {"os_type": "windows"} def send_encrypted_logs_over_socket(config: dict, logs: list): """Streams logs to the LOGAR hub over mTLS or encrypted TCP envelope.""" node_os = get_os_details().get("os_type", "windows") return transport_send_encrypted_logs(config=config, logs=logs, os_type=node_os) def ensure_enrolled(config: dict, machine_id: str) -> bool: """Proactively ensures client certificate enrollment with the Hub if enrollment_secret is available.""" node_os = get_os_details().get("os_type", "windows") return transport_ensure_enrolled(config=config, machine_id=machine_id, os_type=node_os) def main(): parser = argparse.ArgumentParser(description="LOGAR Windows Edge Log Forwarder with State Tracking") parser.add_argument("--config", default=CONFIG_FILE_NAME, help="Path to client_config.json") parser.add_argument("--hours", type=int, default=24, help="Lookback window in hours for event logs (default: 24)") parser.add_argument("--state-file", default=None, help="Path to state tracking file (default: client_state.json next to config)") parser.add_argument("--no-state", action="store_true", help="Disable state tracking and send all events matching lookback window") parser.add_argument("--interval", type=int, default=0, help="Continuous polling interval in seconds (0 = run once and exit, >0 = service loop)") args = parser.parse_args() try: config = load_config(args.config) except Exception as e: print(f"[!] Configuration error: {e}") sys.exit(1) state_path = get_state_path(args.config, args.state_file) state = None if args.no_state else load_state(state_path) machine_id = config.get("client_id") or get_machine_identifier() print(f"[*] Edge Forwarder Node: {machine_id}") if state and "last_record_number" in state: print(f"[*] State tracking active: resuming from record #{state['last_record_number']} (state file: {state_path})") elif not args.no_state: print(f"[*] State tracking initialized (state file: {state_path})") # Proactively ensure dynamic PKI certificate enrollment with Server Hub ensure_enrolled(config, machine_id) collector = WindowsLogCollector(state=state, machine_id=machine_id, channels=["Application"]) def run_forwarder_cycle(): # Continuously verify/renew PKI enrollment if server is licensed & online ensure_enrolled(config, machine_id) # Periodically check server for client auto-updates try: check_and_apply_auto_update(config=config, machine_id=machine_id, os_type="windows") except Exception as update_err: pass print(f"[*] Scanning Windows Application event log for unsent entries (last {args.hours} hours)...") candidate_logs = collector.collect_logs(hours=args.hours) print(f"[*] Found {len(candidate_logs)} unsent candidate entries (INFO to ERROR, entries > {args.hours}h and already-sent skipped).") if not candidate_logs: print("[*] No new unsent events to transmit.") if state is not None: commit_state(state, state_path) return try: resp = send_encrypted_logs_over_socket(config, candidate_logs) if state is not None and resp and resp.get("status") == "success": commit_state(state, state_path) print(f"[+] State successfully committed to {state_path}") except Exception as e: print(f"[!] Failed to stream logs to server: {e}") if not args.interval: sys.exit(1) if args.interval and args.interval > 0: print(f"[*] Starting continuous forwarder service loop (Interval: {args.interval}s)...") while True: try: run_forwarder_cycle() except Exception as e: print(f"[!] Error in forwarder cycle: {e}") time.sleep(args.interval) else: run_forwarder_cycle() if __name__ == "__main__": main()