72 lines
3.4 KiB
MySQL
72 lines
3.4 KiB
MySQL
|
|
-- v1.0.7 item E: raw-payload audit log for every Hyperswitch webhook
|
||
|
|
-- reaching our endpoint. Captures both legitimate deliveries and
|
||
|
|
-- attack attempts (invalid signatures, malformed bodies) — the insert
|
||
|
|
-- happens regardless of signature-valid / processing-success status,
|
||
|
|
-- so a forensics query after "something weird happened last Tuesday"
|
||
|
|
-- has the actual bytes to look at.
|
||
|
|
--
|
||
|
|
-- Shape decisions:
|
||
|
|
--
|
||
|
|
-- payload TEXT — Hyperswitch sends JSON; TEXT is readable in psql
|
||
|
|
-- without base64-decoding and plenty fast at our volumes. Invalid
|
||
|
|
-- UTF-8 is rejected at INSERT time — that class of "attack" is a
|
||
|
|
-- grossly malformed probe where we have the header + ip + timing
|
||
|
|
-- anyway, no value in storing the binary payload.
|
||
|
|
--
|
||
|
|
-- signature_valid BOOLEAN — HMAC verification outcome. Partial
|
||
|
|
-- index below makes "attempts with invalid signature last 24h"
|
||
|
|
-- cheap for forensics.
|
||
|
|
--
|
||
|
|
-- processing_result TEXT — 'ok' on successful dispatch, 'error: <msg>'
|
||
|
|
-- on processing failure (after signature was valid), 'skipped' if
|
||
|
|
-- the handler declined for another reason, 'signature_invalid' if
|
||
|
|
-- rejected at the signature gate.
|
||
|
|
--
|
||
|
|
-- source_ip / user_agent / request_id — forensics essentials.
|
||
|
|
-- request_id is captured from Hyperswitch's X-Request-Id header if
|
||
|
|
-- sent, else the handler generates a UUID so every row has a value
|
||
|
|
-- correlatable against VEZA's structured logs.
|
||
|
|
--
|
||
|
|
-- event_type — pulled from the payload JSON via a best-effort
|
||
|
|
-- extract; NULL if the payload isn't valid JSON or doesn't carry
|
||
|
|
-- an event_type field. Useful for "how many dispute.* events have
|
||
|
|
-- we seen this month" — item P1.6 (disputes) rides along on this
|
||
|
|
-- log without needing its own handler yet.
|
||
|
|
--
|
||
|
|
-- Retention: 90 days by default, swept by CleanupHyperswitchWebhookLog
|
||
|
|
-- (internal/jobs). Configurable via HYPERSWITCH_WEBHOOK_LOG_RETENTION_DAYS.
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS hyperswitch_webhook_log (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
payload TEXT NOT NULL,
|
||
|
|
signature_valid BOOLEAN NOT NULL,
|
||
|
|
signature_header TEXT,
|
||
|
|
processing_result TEXT NOT NULL,
|
||
|
|
event_type TEXT,
|
||
|
|
source_ip TEXT,
|
||
|
|
user_agent TEXT,
|
||
|
|
request_id TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Received-at ordering index: "what did we receive in the last hour"
|
||
|
|
-- is the single most common operational query. Cheap, indexed by
|
||
|
|
-- default PK on id but adding the timestamp index keeps retention
|
||
|
|
-- sweeps and forensics scans well-planned.
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_hyperswitch_webhook_log_received_at
|
||
|
|
ON hyperswitch_webhook_log(received_at DESC);
|
||
|
|
|
||
|
|
-- Partial index on invalid signatures — "show me attack attempts".
|
||
|
|
-- Partial keeps the index tiny on the common case (valid sigs) and
|
||
|
|
-- makes the forensics query instantaneous on the rare case.
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_hyperswitch_webhook_log_signature_invalid
|
||
|
|
ON hyperswitch_webhook_log(received_at DESC)
|
||
|
|
WHERE signature_valid = false;
|
||
|
|
|
||
|
|
-- request_id is required-NOT-NULL at the column level, so an index
|
||
|
|
-- on it is just for "correlate this Veza log line with the webhook
|
||
|
|
-- row". Non-unique because retries with the same request_id could
|
||
|
|
-- land multiple rows (e.g., Hyperswitch redelivers a webhook).
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_hyperswitch_webhook_log_request_id
|
||
|
|
ON hyperswitch_webhook_log(request_id);
|