ce7e040e98
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 7m28s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 26m12s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Successful in 6m33s
208 lines
6.8 KiB
Python
208 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Receive authenticated production health alerts and persist immutable receipts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from http import HTTPStatus
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.parse import unquote, urlparse
|
|
|
|
|
|
TOKEN_PATTERN = re.compile(r"^[A-Za-z0-9_-]{43,128}$")
|
|
DEFAULT_MAX_BODY_BYTES = 1024 * 1024
|
|
|
|
|
|
def read_token(path: Path) -> str:
|
|
token = path.read_text(encoding="utf-8").strip()
|
|
if not TOKEN_PATTERN.fullmatch(token):
|
|
raise ValueError("alert receiver token must be 43-128 URL-safe characters")
|
|
return token
|
|
|
|
|
|
def parse_alert_body(raw: bytes) -> dict[str, Any]:
|
|
try:
|
|
payload = json.loads(raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise ValueError("alert body must be valid UTF-8 JSON") from exc
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("alert body must be a JSON object")
|
|
if payload.get("status") not in {"warning", "fail"}:
|
|
raise ValueError("alert status must be warning or fail")
|
|
if not isinstance(payload.get("checked_at"), str):
|
|
raise ValueError("alert checked_at is required")
|
|
return payload
|
|
|
|
|
|
def persist_event(
|
|
event_dir: Path,
|
|
*,
|
|
payload: dict[str, Any],
|
|
raw: bytes,
|
|
source: str,
|
|
) -> dict[str, str]:
|
|
received_at = datetime.now(timezone.utc).isoformat()
|
|
event_id = f"yuqei-alert-{uuid.uuid4().hex}"
|
|
receipt = {
|
|
"schema_version": "yuqei-production-alert-receipt.v1",
|
|
"event_id": event_id,
|
|
"received_at": received_at,
|
|
"source": source,
|
|
"body_sha256": hashlib.sha256(raw).hexdigest(),
|
|
"payload": payload,
|
|
}
|
|
event_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
target = event_dir / f"{event_id}.json"
|
|
encoded = (json.dumps(receipt, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
descriptor = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
try:
|
|
with os.fdopen(descriptor, "wb") as stream:
|
|
stream.write(encoded)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
except Exception:
|
|
target.unlink(missing_ok=True)
|
|
raise
|
|
return {
|
|
"event_id": event_id,
|
|
"received_at": received_at,
|
|
"body_sha256": receipt["body_sha256"],
|
|
}
|
|
|
|
|
|
class AlertReceiver(ThreadingHTTPServer):
|
|
daemon_threads = True
|
|
|
|
def __init__(
|
|
self,
|
|
server_address: tuple[str, int],
|
|
*,
|
|
token: str,
|
|
event_dir: Path,
|
|
max_body_bytes: int,
|
|
) -> None:
|
|
super().__init__(server_address, AlertHandler)
|
|
self.expected_path = f"/ops/alerts/{token}"
|
|
self.event_dir = event_dir
|
|
self.max_body_bytes = max_body_bytes
|
|
|
|
|
|
class AlertHandler(BaseHTTPRequestHandler):
|
|
server: AlertReceiver
|
|
|
|
def do_POST(self) -> None: # noqa: N802
|
|
request_path = unquote(urlparse(self.path).path)
|
|
if not hmac.compare_digest(request_path, self.server.expected_path):
|
|
self._respond(HTTPStatus.NOT_FOUND, {"error": "not_found"})
|
|
return
|
|
content_type = self.headers.get_content_type()
|
|
if content_type != "application/json":
|
|
self._respond(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, {"error": "application_json_required"})
|
|
return
|
|
try:
|
|
content_length = int(self.headers.get("Content-Length", ""))
|
|
except ValueError:
|
|
content_length = -1
|
|
if not 0 < content_length <= self.server.max_body_bytes:
|
|
self._respond(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, {"error": "invalid_content_length"})
|
|
return
|
|
raw = self.rfile.read(content_length)
|
|
if len(raw) != content_length:
|
|
self._respond(HTTPStatus.BAD_REQUEST, {"error": "incomplete_body"})
|
|
return
|
|
try:
|
|
payload = parse_alert_body(raw)
|
|
receipt = persist_event(
|
|
self.server.event_dir,
|
|
payload=payload,
|
|
raw=raw,
|
|
source=self.headers.get("X-Forwarded-For", self.client_address[0]),
|
|
)
|
|
except (OSError, ValueError):
|
|
self._respond(HTTPStatus.BAD_REQUEST, {"error": "invalid_alert"})
|
|
return
|
|
self._respond(HTTPStatus.CREATED, receipt)
|
|
|
|
def do_GET(self) -> None: # noqa: N802
|
|
self._respond(HTTPStatus.METHOD_NOT_ALLOWED, {"error": "method_not_allowed"})
|
|
|
|
def log_message(self, message: str, *args: object) -> None:
|
|
print(f"ALERT_RECEIVER {self.address_string()} {message % args}", file=sys.stderr)
|
|
|
|
def _respond(self, status: HTTPStatus, payload: dict[str, str]) -> None:
|
|
raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(raw)))
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.send_header("X-Content-Type-Options", "nosniff")
|
|
self.end_headers()
|
|
self.wfile.write(raw)
|
|
|
|
|
|
def build_server(
|
|
bind: str,
|
|
port: int,
|
|
*,
|
|
token_file: Path,
|
|
event_dir: Path,
|
|
max_body_bytes: int,
|
|
) -> AlertReceiver:
|
|
if not 0 <= port <= 65535:
|
|
raise ValueError("port must be between 0 and 65535")
|
|
if not 1024 <= max_body_bytes <= 4 * 1024 * 1024:
|
|
raise ValueError("maximum body size must be between 1 KiB and 4 MiB")
|
|
return AlertReceiver(
|
|
(bind, port),
|
|
token=read_token(token_file),
|
|
event_dir=event_dir.resolve(),
|
|
max_body_bytes=max_body_bytes,
|
|
)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--bind", default="127.0.0.1")
|
|
parser.add_argument("--port", type=int, default=19091)
|
|
parser.add_argument("--token-file", type=Path, required=True)
|
|
parser.add_argument("--event-dir", type=Path, required=True)
|
|
parser.add_argument("--max-body-bytes", type=int, default=DEFAULT_MAX_BODY_BYTES)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
try:
|
|
server = build_server(
|
|
args.bind,
|
|
args.port,
|
|
token_file=args.token_file,
|
|
event_dir=args.event_dir,
|
|
max_body_bytes=args.max_body_bytes,
|
|
)
|
|
except (OSError, ValueError) as exc:
|
|
print(f"ALERT_RECEIVER_FAIL error={type(exc).__name__}", file=sys.stderr)
|
|
return 2
|
|
print(f"ALERT_RECEIVER_READY bind={args.bind} port={args.port}", flush=True)
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
server.server_close()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|