Catalyst / admin/Syanpse-Vanguard 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Syanpse-Vanguard

public
Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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()