Bez popisu

mvp_repo.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. from __future__ import annotations
  2. from datetime import datetime, timezone
  3. from typing import Any
  4. from psycopg.types.json import Json
  5. from app.db import get_conn
  6. def utc_now() -> datetime:
  7. return datetime.now(timezone.utc)
  8. DEFAULT_POLICY: dict[str, Any] = {
  9. "escalate_severities": ["high", "critical"],
  10. "vpn": {
  11. "allowed_country": "TH",
  12. "exception_users": [],
  13. },
  14. "risk": {
  15. "weights": {
  16. "outside_thailand": 50,
  17. "admin": 20,
  18. "off_hours": 15,
  19. "first_seen_country": 15,
  20. },
  21. "thresholds": {
  22. "high": 70,
  23. "medium": 40,
  24. },
  25. },
  26. "shuffle": {
  27. "ioc_workflow_id": "",
  28. },
  29. }
  30. class MvpRepository:
  31. def count_incident_events_since(self, since: datetime, source: str | None = None) -> int:
  32. with get_conn() as conn, conn.cursor() as cur:
  33. if source:
  34. cur.execute(
  35. """
  36. SELECT COUNT(*) AS cnt
  37. FROM incident_events
  38. WHERE created_at >= %s
  39. AND source = %s
  40. """,
  41. (since, source),
  42. )
  43. else:
  44. cur.execute(
  45. """
  46. SELECT COUNT(*) AS cnt
  47. FROM incident_events
  48. WHERE created_at >= %s
  49. """,
  50. (since,),
  51. )
  52. row = cur.fetchone() or {}
  53. return int(row.get("cnt", 0) or 0)
  54. def count_incidents_with_iris_since(self, since: datetime) -> int:
  55. with get_conn() as conn, conn.cursor() as cur:
  56. cur.execute(
  57. """
  58. SELECT COUNT(*) AS cnt
  59. FROM incident_index
  60. WHERE first_seen >= %s
  61. AND iris_case_id IS NOT NULL
  62. AND iris_case_id <> ''
  63. """,
  64. (since,),
  65. )
  66. row = cur.fetchone() or {}
  67. return int(row.get("cnt", 0) or 0)
  68. def count_escalations_since(self, since: datetime, success: bool | None = None) -> int:
  69. with get_conn() as conn, conn.cursor() as cur:
  70. if success is None:
  71. cur.execute(
  72. """
  73. SELECT COUNT(*) AS cnt
  74. FROM escalation_audit
  75. WHERE attempted_at >= %s
  76. """,
  77. (since,),
  78. )
  79. else:
  80. cur.execute(
  81. """
  82. SELECT COUNT(*) AS cnt
  83. FROM escalation_audit
  84. WHERE attempted_at >= %s
  85. AND success = %s
  86. """,
  87. (since, success),
  88. )
  89. row = cur.fetchone() or {}
  90. return int(row.get("cnt", 0) or 0)
  91. def count_c_detection_events_since(self, since: datetime) -> int:
  92. with get_conn() as conn, conn.cursor() as cur:
  93. cur.execute(
  94. """
  95. SELECT COUNT(*) AS cnt
  96. FROM c_detection_events
  97. WHERE matched_at >= %s
  98. """,
  99. (since,),
  100. )
  101. row = cur.fetchone() or {}
  102. return int(row.get("cnt", 0) or 0)
  103. def list_recent_escalations(self, limit: int = 20) -> list[dict[str, Any]]:
  104. with get_conn() as conn, conn.cursor() as cur:
  105. cur.execute(
  106. """
  107. SELECT id, incident_key, attempted_at, status_code, success, response_excerpt
  108. FROM escalation_audit
  109. ORDER BY attempted_at DESC
  110. LIMIT %s
  111. """,
  112. (max(1, limit),),
  113. )
  114. return [dict(row) for row in cur.fetchall()]
  115. def has_event(self, source: str, event_id: str) -> bool:
  116. with get_conn() as conn, conn.cursor() as cur:
  117. cur.execute(
  118. "SELECT 1 FROM incident_events WHERE source = %s AND event_id = %s LIMIT 1",
  119. (source, event_id),
  120. )
  121. return cur.fetchone() is not None
  122. def ensure_policy(self) -> None:
  123. with get_conn() as conn, conn.cursor() as cur:
  124. cur.execute("SELECT id FROM policy_config WHERE id = 1")
  125. found = cur.fetchone()
  126. if not found:
  127. cur.execute(
  128. "INSERT INTO policy_config(id, data) VALUES (1, %s)",
  129. (Json(DEFAULT_POLICY),),
  130. )
  131. def get_policy(self) -> dict[str, Any]:
  132. self.ensure_policy()
  133. with get_conn() as conn, conn.cursor() as cur:
  134. cur.execute("SELECT data FROM policy_config WHERE id = 1")
  135. row = cur.fetchone()
  136. return dict(row["data"]) if row else dict(DEFAULT_POLICY)
  137. def update_policy(self, data: dict[str, Any]) -> dict[str, Any]:
  138. with get_conn() as conn, conn.cursor() as cur:
  139. cur.execute(
  140. """
  141. INSERT INTO policy_config(id, data, updated_at)
  142. VALUES (1, %s, NOW())
  143. ON CONFLICT (id)
  144. DO UPDATE SET data = EXCLUDED.data, updated_at = NOW()
  145. RETURNING data
  146. """,
  147. (Json(data),),
  148. )
  149. row = cur.fetchone()
  150. return dict(row["data"])
  151. def get_incident(self, incident_key: str) -> dict[str, Any] | None:
  152. with get_conn() as conn, conn.cursor() as cur:
  153. cur.execute(
  154. "SELECT incident_key, iris_case_id, status, severity, first_seen, last_seen FROM incident_index WHERE incident_key = %s",
  155. (incident_key,),
  156. )
  157. row = cur.fetchone()
  158. return dict(row) if row else None
  159. def upsert_incident(
  160. self,
  161. incident_key: str,
  162. severity: str,
  163. status: str,
  164. iris_case_id: str | None,
  165. ) -> dict[str, Any]:
  166. now = utc_now()
  167. with get_conn() as conn, conn.cursor() as cur:
  168. cur.execute(
  169. """
  170. INSERT INTO incident_index(incident_key, iris_case_id, status, severity, first_seen, last_seen)
  171. VALUES (%s, %s, %s, %s, %s, %s)
  172. ON CONFLICT (incident_key)
  173. DO UPDATE SET
  174. iris_case_id = COALESCE(EXCLUDED.iris_case_id, incident_index.iris_case_id),
  175. status = EXCLUDED.status,
  176. severity = EXCLUDED.severity,
  177. last_seen = EXCLUDED.last_seen
  178. RETURNING incident_key, iris_case_id, status, severity, first_seen, last_seen
  179. """,
  180. (incident_key, iris_case_id, status, severity, now, now),
  181. )
  182. return dict(cur.fetchone())
  183. def add_event(
  184. self,
  185. incident_key: str,
  186. event_id: str | None,
  187. source: str,
  188. event_type: str,
  189. raw_payload: dict[str, Any],
  190. decision_trace: dict[str, Any],
  191. ) -> None:
  192. with get_conn() as conn, conn.cursor() as cur:
  193. cur.execute(
  194. """
  195. INSERT INTO incident_events(incident_key, event_id, source, event_type, raw_payload, decision_trace)
  196. VALUES (%s, %s, %s, %s, %s, %s)
  197. """,
  198. (
  199. incident_key,
  200. event_id,
  201. source,
  202. event_type,
  203. Json(raw_payload),
  204. Json(decision_trace),
  205. ),
  206. )
  207. def add_escalation_audit(
  208. self,
  209. incident_key: str,
  210. status_code: int | None,
  211. success: bool,
  212. response_excerpt: str | None,
  213. ) -> None:
  214. with get_conn() as conn, conn.cursor() as cur:
  215. cur.execute(
  216. """
  217. INSERT INTO escalation_audit(incident_key, status_code, success, response_excerpt)
  218. VALUES (%s, %s, %s, %s)
  219. """,
  220. (incident_key, status_code, success, response_excerpt),
  221. )
  222. def add_ioc_trace(
  223. self,
  224. action: str,
  225. ioc_type: str,
  226. ioc_value: str,
  227. providers: list[str],
  228. request_payload: dict[str, Any],
  229. response_payload: dict[str, Any],
  230. matched: bool | None = None,
  231. severity: str | None = None,
  232. confidence: float | None = None,
  233. error: str | None = None,
  234. ) -> None:
  235. with get_conn() as conn, conn.cursor() as cur:
  236. cur.execute(
  237. """
  238. INSERT INTO ioc_trace(
  239. action, ioc_type, ioc_value, providers,
  240. request_payload, response_payload, matched, severity, confidence, error
  241. )
  242. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
  243. """,
  244. (
  245. action,
  246. ioc_type,
  247. ioc_value,
  248. Json(providers),
  249. Json(request_payload),
  250. Json(response_payload),
  251. matched,
  252. severity,
  253. confidence,
  254. error,
  255. ),
  256. )
  257. def list_ioc_trace(self, limit: int = 50, offset: int = 0) -> list[dict[str, Any]]:
  258. with get_conn() as conn, conn.cursor() as cur:
  259. cur.execute(
  260. """
  261. SELECT id, action, ioc_type, ioc_value, providers, matched, severity, confidence, error, created_at
  262. FROM ioc_trace
  263. ORDER BY created_at DESC
  264. LIMIT %s OFFSET %s
  265. """,
  266. (max(1, limit), max(0, offset)),
  267. )
  268. return [dict(row) for row in cur.fetchall()]
  269. def get_correlation_state(self, entity_key: str) -> dict[str, Any] | None:
  270. with get_conn() as conn, conn.cursor() as cur:
  271. cur.execute(
  272. "SELECT state FROM correlation_state WHERE entity_key = %s",
  273. (entity_key,),
  274. )
  275. row = cur.fetchone()
  276. return dict(row["state"]) if row else None
  277. def upsert_correlation_state(self, entity_key: str, state: dict[str, Any]) -> None:
  278. with get_conn() as conn, conn.cursor() as cur:
  279. cur.execute(
  280. """
  281. INSERT INTO correlation_state(entity_key, state, updated_at)
  282. VALUES (%s, %s, NOW())
  283. ON CONFLICT (entity_key)
  284. DO UPDATE SET state = EXCLUDED.state, updated_at = NOW()
  285. """,
  286. (entity_key, Json(state)),
  287. )
  288. def add_c_detection_event(
  289. self,
  290. usecase_id: str,
  291. entity: str,
  292. severity: str,
  293. evidence: dict[str, Any],
  294. event_ref: dict[str, Any],
  295. incident_key: str | None = None,
  296. ) -> dict[str, Any]:
  297. with get_conn() as conn, conn.cursor() as cur:
  298. cur.execute(
  299. """
  300. INSERT INTO c_detection_events(usecase_id, entity, severity, evidence, event_ref, incident_key)
  301. VALUES (%s, %s, %s, %s, %s, %s)
  302. RETURNING id, usecase_id, entity, severity, evidence, event_ref, incident_key, matched_at
  303. """,
  304. (
  305. usecase_id,
  306. entity,
  307. severity,
  308. Json(evidence),
  309. Json(event_ref),
  310. incident_key,
  311. ),
  312. )
  313. return dict(cur.fetchone())
  314. def update_c_detection_incident(self, event_id: int, incident_key: str | None) -> None:
  315. with get_conn() as conn, conn.cursor() as cur:
  316. cur.execute(
  317. "UPDATE c_detection_events SET incident_key = %s WHERE id = %s",
  318. (incident_key, event_id),
  319. )
  320. def list_c_detection_events(
  321. self,
  322. limit: int = 50,
  323. offset: int = 0,
  324. usecase_id: str | None = None,
  325. ) -> list[dict[str, Any]]:
  326. with get_conn() as conn, conn.cursor() as cur:
  327. if usecase_id:
  328. cur.execute(
  329. """
  330. SELECT id, usecase_id, entity, severity, evidence, event_ref, incident_key, matched_at
  331. FROM c_detection_events
  332. WHERE usecase_id = %s
  333. ORDER BY matched_at DESC
  334. LIMIT %s OFFSET %s
  335. """,
  336. (usecase_id, max(1, limit), max(0, offset)),
  337. )
  338. else:
  339. cur.execute(
  340. """
  341. SELECT id, usecase_id, entity, severity, evidence, event_ref, incident_key, matched_at
  342. FROM c_detection_events
  343. ORDER BY matched_at DESC
  344. LIMIT %s OFFSET %s
  345. """,
  346. (max(1, limit), max(0, offset)),
  347. )
  348. return [dict(row) for row in cur.fetchall()]
  349. def is_c_detection_in_cooldown(self, usecase_id: str, entity: str, cooldown_seconds: int) -> bool:
  350. if cooldown_seconds <= 0:
  351. return False
  352. with get_conn() as conn, conn.cursor() as cur:
  353. cur.execute(
  354. """
  355. SELECT 1
  356. FROM c_detection_events
  357. WHERE usecase_id = %s
  358. AND entity = %s
  359. AND incident_key IS NOT NULL
  360. AND matched_at > (NOW() - (%s || ' seconds')::interval)
  361. LIMIT 1
  362. """,
  363. (usecase_id, entity, int(cooldown_seconds)),
  364. )
  365. return cur.fetchone() is not None