audit-core/audit_core/ingestion.py
tegwick be85ce7c43
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Implement durable event ingestion service
2026-08-09 20:59:14 +02:00

126 lines
5.3 KiB
Python

"""Authenticated, idempotent HTTP ingestion for user-engine outbox events."""
from __future__ import annotations
import hashlib
import hmac
import json
import os
import sqlite3
from datetime import datetime
from http import HTTPStatus
from typing import Any
from wsgiref.simple_server import make_server
from audit_core.interface import AuditEvent
MAX_BODY_BYTES = 256 * 1024
_SECRET_FRAGMENTS = ("password", "secret", "token", "credential", "private_key")
class SQLiteEventStore:
def __init__(self, path: str) -> None:
self.db = sqlite3.connect(path, check_same_thread=False)
self.db.execute("""
CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY, payload_hash TEXT NOT NULL,
accepted_at TEXT NOT NULL, record TEXT NOT NULL
)
""")
self.db.commit()
def accept(self, event: AuditEvent, payload_hash: str) -> tuple[bool, str]:
existing = self.db.execute(
"SELECT payload_hash FROM events WHERE event_id = ?", (event.event_id,)
).fetchone()
if existing:
if existing[0] != payload_hash:
raise ValueError("event_id_conflict")
return True, f"audit:{event.event_id}"
with self.db:
self.db.execute(
"INSERT INTO events VALUES (?, ?, ?, ?)",
(event.event_id, payload_hash, datetime.now().astimezone().isoformat(),
json.dumps(event.as_record(), sort_keys=True)),
)
return False, f"audit:{event.event_id}"
class IngestionApplication:
def __init__(self, store: SQLiteEventStore, bearer_token: str) -> None:
if not bearer_token:
raise ValueError("bearer token is required")
self.store = store
self.token = bearer_token
def __call__(self, environ, start_response):
path = environ.get("PATH_INFO", "")
if path in ("/healthz", "/readyz"):
return self._json(start_response, HTTPStatus.OK, {"status": "ok"})
if path != "/v1/events" or environ.get("REQUEST_METHOD") != "POST":
return self._json(start_response, HTTPStatus.NOT_FOUND, {"error": "not_found"})
supplied = str(environ.get("HTTP_AUTHORIZATION", ""))
if not hmac.compare_digest(supplied, f"Bearer {self.token}"):
return self._json(start_response, HTTPStatus.UNAUTHORIZED, {"error": "unauthorized"})
try:
length = int(environ.get("CONTENT_LENGTH") or 0)
if length <= 0 or length > MAX_BODY_BYTES:
raise ValueError("invalid_size")
raw = environ["wsgi.input"].read(length)
payload = json.loads(raw)
event = normalize(payload, environ.get("HTTP_IDEMPOTENCY_KEY"))
duplicate, reference = self.store.accept(
event, hashlib.sha256(raw).hexdigest()
)
except (ValueError, TypeError, KeyError, json.JSONDecodeError) as exc:
return self._json(start_response, HTTPStatus.BAD_REQUEST, {"error": str(exc)})
return self._json(start_response, HTTPStatus.OK if duplicate else HTTPStatus.ACCEPTED,
{"status": "duplicate" if duplicate else "accepted",
"reference": reference})
@staticmethod
def _json(start_response, status: HTTPStatus, payload: dict[str, Any]):
body = json.dumps(payload).encode()
start_response(f"{status.value} {status.phrase}", [
("Content-Type", "application/json"), ("Content-Length", str(len(body)))
])
return [body]
def normalize(payload: dict[str, Any], idempotency_key: str | None) -> AuditEvent:
required = ("id", "type", "source", "subject", "tenant", "correlation_id", "occurred_at", "data")
if not isinstance(payload, dict) or any(not payload.get(key) for key in required):
raise ValueError("invalid_event")
if idempotency_key != payload["id"]:
raise ValueError("idempotency_key_mismatch")
if payload["source"] != "user-engine":
raise ValueError("source_not_allowed")
datetime.fromisoformat(str(payload["occurred_at"]).replace("Z", "+00:00"))
if _contains_secret(payload["data"]):
raise ValueError("secret_shaped_field")
return AuditEvent(
event_id=str(payload["id"]), observed_at=str(payload["occurred_at"]),
tenant=str(payload["tenant"]), scope="tenant", source="user-engine",
action=str(payload["type"]), resource=str(payload["subject"]),
outcome="recorded", actor=None,
details={"correlation_id": str(payload["correlation_id"]), "data": payload["data"]},
)
def _contains_secret(value: Any) -> bool:
if isinstance(value, dict):
return any(any(fragment in str(key).lower() for fragment in _SECRET_FRAGMENTS)
or _contains_secret(item) for key, item in value.items())
if isinstance(value, list):
return any(_contains_secret(item) for item in value)
return False
def main() -> None:
app = IngestionApplication(
SQLiteEventStore(os.environ.get("AUDIT_CORE_DATABASE_PATH", "/data/audit-core.db")),
os.environ["AUDIT_CORE_INGEST_TOKEN"].strip(),
)
with make_server(os.environ.get("AUDIT_CORE_HOST", "0.0.0.0"),
int(os.environ.get("AUDIT_CORE_HTTP_PORT", "8080")), app) as server:
server.serve_forever()