admin / Syanpse-Vanguard
public
Syanpse-Vanguard / synapse-vanguard-v3 / sv / query / builder.py
2576 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 | """Structured search -> parameterized Query. Field names are whitelisted and all values are bound parameters, so user input can never reach the SQL text. This is where the future NL->SQL translator will emit its output too.""" from __future__ import annotations from datetime import datetime from typing import Any from uuid import UUID from pydantic import BaseModel, Field from sv.storage.base import Query # Only these columns may be filtered/sorted on. Anything else is rejected before # a query is built — an allowlist, not an escaping scheme. _FILTERABLE = { "source_type", "host", "host_ip", "event_id", "severity", "category", "action", "outcome", "user_name", "process_name", "mitre_technique", } _SORTABLE = {"ts", "ingested_ts", "severity", "event_id"} _MAX_LIMIT = 10_000 class SearchRequest(BaseModel): start: datetime | None = None end: datetime | None = None equals: dict[str, Any] = Field(default_factory=dict) # exact-match filters contains: str | None = None # substring on message sort_by: str = "ts" descending: bool = True limit: int = Field(default=200, ge=1, le=_MAX_LIMIT) offset: int = Field(default=0, ge=0) def build_search(req: SearchRequest, tenant_id: UUID) -> Query: where: list[str] = ["tenant_id = ?"] params: list[Any] = [str(tenant_id)] # tenant isolation is always enforced if req.start is not None: where.append("ts >= ?") params.append(_naive(req.start)) if req.end is not None: where.append("ts <= ?") params.append(_naive(req.end)) for field_name, value in req.equals.items(): if field_name not in _FILTERABLE: raise ValueError(f"field not filterable: {field_name}") where.append(f"{field_name} = ?") params.append(value) if req.contains: where.append("message ILIKE ?") params.append(f"%{req.contains}%") sort_by = req.sort_by if req.sort_by in _SORTABLE else "ts" direction = "DESC" if req.descending else "ASC" text = ( "SELECT ts, ingested_ts, source_type, host, host_ip, event_id, " "severity, category, action, outcome, user_name, process_name, " "mitre_technique, message " "FROM events " f"WHERE {' AND '.join(where)} " f"ORDER BY {sort_by} {direction} " "LIMIT ? OFFSET ?" ) params.extend([req.limit, req.offset]) return Query(text=text, params=params) def _naive(dt: datetime) -> datetime: return dt.replace(tzinfo=None) if dt.tzinfo else dt |