security_delivery.py 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. """Restricted outbound transports for security audit evidence."""
  2. from __future__ import annotations
  3. import json
  4. import socket
  5. import ssl
  6. from urllib.parse import urlsplit
  7. from urllib.request import HTTPRedirectHandler, HTTPSHandler, Request, build_opener
  8. class _NoRedirect(HTTPRedirectHandler):
  9. def redirect_request(self, req, fp, code, msg, headers, newurl):
  10. return None
  11. class HttpsWebhookSyslogTransport:
  12. """Deliver bounded safe envelopes after service-level endpoint validation."""
  13. def __init__(self, *, timeout: float = 5.0, maximum_bytes: int = 1_048_576):
  14. self.timeout = float(timeout)
  15. self.maximum_bytes = int(maximum_bytes)
  16. def deliver(self, sink, envelope):
  17. payload = json.dumps(
  18. envelope, ensure_ascii=False, sort_keys=True, separators=(",", ":")
  19. ).encode("utf-8")
  20. if len(payload) > self.maximum_bytes:
  21. return {"status": "failed", "error_code": "payload_too_large"}
  22. try:
  23. if sink["sink_type"] == "webhook":
  24. return self._webhook(sink["endpoint"], payload)
  25. return self._syslog_tls(sink["endpoint"], payload)
  26. except (OSError, ssl.SSLError, TimeoutError):
  27. return {"status": "failed", "error_code": "transport_unavailable"}
  28. def _webhook(self, endpoint, payload):
  29. request = Request(
  30. endpoint,
  31. data=payload,
  32. headers={"Content-Type": "application/json", "User-Agent": "dataops-security/1"},
  33. method="POST",
  34. )
  35. opener = build_opener(HTTPSHandler(), _NoRedirect())
  36. with opener.open(request, timeout=self.timeout) as response:
  37. status = int(response.status)
  38. if status < 200 or status >= 300:
  39. return {"status": "failed", "error_code": f"http_{status}"}
  40. remote_ref = str(response.headers.get("X-Request-ID") or "")[:300] or None
  41. return {"status": "delivered", "remote_ref": remote_ref}
  42. def _syslog_tls(self, endpoint, payload):
  43. parsed = urlsplit(endpoint)
  44. port = int(parsed.port or 6514)
  45. context = ssl.create_default_context()
  46. with (
  47. socket.create_connection((parsed.hostname, port), timeout=self.timeout) as raw,
  48. context.wrap_socket(raw, server_hostname=parsed.hostname) as secured,
  49. ):
  50. secured.sendall(payload + b"\n")
  51. return {"status": "delivered"}