poc2_worked
This commit is contained in:
+120
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
|
||||
class WebhookNotifier:
|
||||
"""Bounded asynchronous JSON webhook delivery for high-risk NDR incidents."""
|
||||
|
||||
def __init__(self, url: str, min_risk: int = 80, timeout: int = 5) -> None:
|
||||
self.url = str(url or "").strip()
|
||||
self.min_risk = max(1, min(100, int(min_risk)))
|
||||
self.timeout = max(1, min(30, int(timeout)))
|
||||
self.enabled = self.url.startswith(("http://", "https://"))
|
||||
self._queue: queue.Queue[dict[str, Any]] = queue.Queue(maxsize=256)
|
||||
self._stop = threading.Event()
|
||||
self._thread = threading.Thread(target=self._run, name="ndr-webhook", daemon=True)
|
||||
self._sent_state: dict[int, tuple[int, float]] = {}
|
||||
self._sent = 0
|
||||
self._failed = 0
|
||||
self._dropped = 0
|
||||
self._last_error = ""
|
||||
self._last_sent_at = ""
|
||||
|
||||
def start(self) -> None:
|
||||
if self.enabled and not self._thread.is_alive():
|
||||
self._thread.start()
|
||||
|
||||
def stop(self, timeout: float = 3.0) -> None:
|
||||
self._stop.set()
|
||||
if self._thread.is_alive():
|
||||
self._thread.join(timeout=timeout)
|
||||
|
||||
def notify(self, incident: dict[str, Any], evidence: dict[str, Any]) -> None:
|
||||
if not self.enabled:
|
||||
return
|
||||
risk = int(incident.get("risk_score") or 0)
|
||||
incident_id = int(incident.get("id") or 0)
|
||||
if risk < self.min_risk or incident_id <= 0:
|
||||
return
|
||||
now = time.monotonic()
|
||||
previous_risk, previous_at = self._sent_state.get(incident_id, (0, 0.0))
|
||||
# Re-notify only if risk meaningfully escalated or 15 minutes passed.
|
||||
if previous_at and risk < previous_risk + 10 and now - previous_at < 900:
|
||||
return
|
||||
payload = {
|
||||
"event": "mikrosuricata.ndr.incident",
|
||||
"incident": {
|
||||
"id": incident_id,
|
||||
"subject_ip": incident.get("subject_ip"),
|
||||
"risk_score": risk,
|
||||
"severity": incident.get("severity"),
|
||||
"status": incident.get("status"),
|
||||
"title": incident.get("title"),
|
||||
"summary": incident.get("summary"),
|
||||
"stages": incident.get("stages") or [],
|
||||
"destinations": incident.get("destinations") or [],
|
||||
"blocked": bool(incident.get("blocked")),
|
||||
"block_target": incident.get("block_target"),
|
||||
"last_seen": incident.get("last_seen"),
|
||||
},
|
||||
"evidence": {
|
||||
"kind": evidence.get("kind"),
|
||||
"stage": evidence.get("stage"),
|
||||
"risk": evidence.get("risk"),
|
||||
"summary": evidence.get("summary"),
|
||||
"src_ip": evidence.get("src_ip"),
|
||||
"dest_ip": evidence.get("dest_ip"),
|
||||
"signature_id": evidence.get("signature_id"),
|
||||
},
|
||||
}
|
||||
try:
|
||||
self._queue.put_nowait(payload)
|
||||
self._sent_state[incident_id] = (risk, now)
|
||||
except queue.Full:
|
||||
self._dropped += 1
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"running": self._thread.is_alive(),
|
||||
"min_risk": self.min_risk,
|
||||
"queue": self._queue.qsize(),
|
||||
"sent": self._sent,
|
||||
"failed": self._failed,
|
||||
"dropped": self._dropped,
|
||||
"last_sent_at": self._last_sent_at,
|
||||
"last_error": self._last_error,
|
||||
}
|
||||
|
||||
def _run(self) -> None:
|
||||
while not self._stop.is_set() or not self._queue.empty():
|
||||
try:
|
||||
payload = self._queue.get(timeout=0.25)
|
||||
except queue.Empty:
|
||||
continue
|
||||
try:
|
||||
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
self.url,
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json", "User-Agent": "MikroSuricata-NDR/0.8"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
||||
if int(getattr(response, "status", 200)) >= 400:
|
||||
raise urllib.error.HTTPError(self.url, response.status, "webhook error", response.headers, None)
|
||||
self._sent += 1
|
||||
self._last_sent_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
self._last_error = ""
|
||||
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as exc:
|
||||
self._failed += 1
|
||||
self._last_error = str(exc)[:300]
|
||||
finally:
|
||||
self._queue.task_done()
|
||||
Reference in New Issue
Block a user