admin / Syanpse-Vanguard
public
Syanpse-Vanguard / synapse-vanguard-v3 / agents / windows / sv_agent_windows.py
5165 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """Synapse-Vanguard Windows agent. Ingests Windows Event Log channels (Security, System, Application, Sysmon). Two collection strategies, auto-selected: 1. pywin32 (win32evtlog) — preferred, native subscription. Used if importable. 2. PowerShell Get-WinEvent poller — zero-native-dep fallback; polls each channel and advances a per-channel RecordId bookmark. Package as a standalone executable with: pyinstaller --onefile --name sv-agent-windows ^ --collect-submodules agents agents\\windows\\sv_agent_windows.py """ from __future__ import annotations import json import logging import subprocess import threading import time from datetime import datetime, timezone from agents.common.buffer import DiskBuffer from agents.common.config import AgentConfig from agents.common.sender import Sender log = logging.getLogger("sv.agent.windows") def _emit(buffer: DiskBuffer, host: str, message: str, fields: dict, ts: datetime | None = None) -> None: buffer.append({ "source_type": "winlog", "host": host, "ts": (ts or datetime.now(timezone.utc)).isoformat(), "message": message, "fields": fields, }) # --- PowerShell fallback poller --------------------------------------------- _PS_TEMPLATE = ( "$ErrorActionPreference='SilentlyContinue';" "Get-WinEvent -FilterHashtable @{{LogName='{channel}'}} -MaxEvents 200 |" " Where-Object {{ $_.RecordId -gt {last_id} }} |" " Sort-Object RecordId |" " Select-Object RecordId,Id,LevelDisplayName,Level,ProviderName," "TimeCreated,MachineName,Message,Task |" " ConvertTo-Json -Depth 3 -Compress" ) def poll_channel_powershell(cfg: AgentConfig, buffer: DiskBuffer, channel: str) -> None: last_id = 0 while True: try: script = _PS_TEMPLATE.format(channel=channel, last_id=last_id) out = subprocess.run( ["powershell", "-NoProfile", "-NonInteractive", "-Command", script], capture_output=True, text=True, timeout=60, ).stdout.strip() if out: records = json.loads(out) if isinstance(records, dict): # single record -> wrap records = [records] for rec in records: rid = int(rec.get("RecordId", 0) or 0) last_id = max(last_id, rid) _emit( buffer, rec.get("MachineName") or cfg.hostname, rec.get("Message", "") or "", { "Channel": channel, "EventID": rec.get("Id"), "Level": rec.get("Level"), "Task": rec.get("Task"), "Provider": rec.get("ProviderName"), "RecordId": rid, }, _win_ts(rec.get("TimeCreated")), ) except Exception as exc: log.warning("channel %s poll error: %s", channel, exc) time.sleep(5) def _run_pywin32(cfg: AgentConfig, buffer: DiskBuffer) -> bool: """Return True if the native path is available and started.""" try: import win32evtlog # noqa: F401 (import test only) except ImportError: return False def _subscribe(channel: str) -> None: import win32evtlog # Pull-style subscription from the tail of the channel. query = win32evtlog.EvtQuery( channel, win32evtlog.EvtQueryReverseDirection) # Production: use EvtSubscribe with a bookmark for push delivery. Kept # brief here; the PowerShell poller is the exercised fallback. del query for ch in cfg.winlog_channels: threading.Thread(target=_subscribe, args=(ch,), daemon=True).start() log.info("using native win32evtlog collectors") return True def main() -> None: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s") cfg = AgentConfig.load() cfg.platform = "windows" if not cfg.channels: cfg.channels = cfg.winlog_channels buffer = DiskBuffer(cfg.buffer_path) if not _run_pywin32(cfg, buffer): log.info("pywin32 unavailable; using PowerShell Get-WinEvent poller") for ch in cfg.winlog_channels: threading.Thread(target=poll_channel_powershell, args=(cfg, buffer, ch), daemon=True).start() Sender(cfg, buffer).run_forever() def _win_ts(val) -> datetime | None: if not val: return None # PowerShell ConvertTo-Json emits /Date(ms)/ or ISO depending on version. s = str(val) if s.startswith("/Date("): try: ms = int(s[6:s.index(")")]) return datetime.fromtimestamp(ms / 1000, tz=timezone.utc) except (ValueError, IndexError): return None try: return datetime.fromisoformat(s.replace("Z", "+00:00")) except ValueError: return None if __name__ == "__main__": main() |