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 / sv / models / event.py 3259 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
"""Normalized security-event model (ECS-flavored) and the raw agent event that
the normalizer turns into it."""
from __future__ import annotations

from datetime import datetime, timezone
from enum import IntEnum
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field

from sv.config import DEFAULT_TENANT_ID


class Severity(IntEnum):
    EMERGENCY = 0
    ALERT = 1
    CRITICAL = 2
    ERROR = 3
    WARNING = 4
    NOTICE = 5
    INFO = 6
    DEBUG = 7


# Column order is the single source of truth for the Arrow batch and the
# INSERT column list. Keep it in sync with schema.sql.
STORAGE_COLUMNS: list[str] = [
    "ts", "ingested_ts", "tenant_id", "source_type", "host", "host_ip",
    "event_id", "severity", "category", "action", "outcome", "user_name",
    "process_name", "mitre_technique", "message", "raw", "labels",
]


class RawEvent(BaseModel):
    """What an agent ships on the wire — one NDJSON line per event. Deliberately
    permissive; the normalizer is responsible for mapping to EventModel."""
    model_config = ConfigDict(extra="allow")

    source_type: str                     # winlog | journald | syslog | auditd ...
    host: str
    ts: datetime | None = None           # agent-provided event time (optional)
    message: str = ""
    # channel-specific fields land in `fields` or as extras
    fields: dict = Field(default_factory=dict)


class EventModel(BaseModel):
    """The normalized, storable event."""
    model_config = ConfigDict(extra="forbid")

    ts: datetime
    ingested_ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    tenant_id: UUID = DEFAULT_TENANT_ID
    source_type: str
    host: str
    host_ip: str | None = None
    event_id: int | None = None
    severity: Severity = Severity.INFO
    category: str | None = None
    action: str | None = None
    outcome: str | None = None
    user_name: str | None = None
    process_name: str | None = None
    mitre_technique: str | None = None
    message: str = ""
    raw: dict = Field(default_factory=dict)
    labels: dict[str, str] = Field(default_factory=dict)

    @staticmethod
    def storage_columns() -> list[str]:
        return STORAGE_COLUMNS

    def to_storage_row(self) -> dict:
        import json
        return {
            "ts": _naive_utc(self.ts),
            "ingested_ts": _naive_utc(self.ingested_ts),
            "tenant_id": str(self.tenant_id),
            "source_type": self.source_type,
            "host": self.host,
            "host_ip": self.host_ip,
            "event_id": self.event_id,
            "severity": int(self.severity),
            "category": self.category,
            "action": self.action,
            "outcome": self.outcome,
            "user_name": self.user_name,
            "process_name": self.process_name,
            "mitre_technique": self.mitre_technique,
            "message": self.message,
            "raw": json.dumps(self.raw, default=str),
            "labels": json.dumps(self.labels, default=str),
        }


def _naive_utc(dt: datetime) -> datetime:
    """DuckDB TIMESTAMP is timezone-naive; store everything as naive UTC."""
    if dt.tzinfo is not None:
        dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
    return dt