feat(webhook-relay): read the alerts vendors push to riposte-relay (1.0.0)

Riposte accepts no inbound connection, so a vendor that can only push
(Rapid7 InsightIDR Essential basic detection rules) posts to the relay,
and an alert rule on this integration reads one relay source back over
outbound HTTPS.

fetch_messages takes the rule's watermark as `since` and walks every
page within the run, since the next watermark is this run's instant. A
window holding more than `limit` fails the run instead of truncating, so
the watermark stays put. relay_status and test_connection tell a 404
from the proxy (this Riposte's address is not listed) from a refused
token. The generic mapper keeps the raw body, the sender's address and
the checks the delivery passed (custom:webhook_verified).

Tested against a local relay: 2503 messages over three pages, a
millisecond watermark, the limit, unknown source and bad token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xa1QDbsPTYiVREv2Cvs3Zn
This commit is contained in:
2026-10-16 11:23:03 +00:00
parent ff461464fd
commit 6b76270cc6
6 changed files with 230 additions and 0 deletions
@@ -0,0 +1,35 @@
name: "Webhook Relay message (generic)"
description: |
Maps one relayed message — whatever the vendor sent — to an alert, without knowing
the vendor. It is the mapper a new source starts with: the alert exists, the raw
payload is readable in Riposte, and a vendor mapper is then written from it
(clone this one, or pick the vendor's own when the marketplace ships it).
A message is {id, source, received_at, remote_ip, content_type, headers, verified,
body | body_b64, json}. `json` is the body already decoded when it is JSON, so a
vendor field reads json.<field>.
The title tries the fields alert payloads most often name themselves with, and
falls back to the source. Severity is left to the alert rule's default: a generic
payload has no scale this mapper could trust.
custom:webhook_verified is the trust level: the checks the delivery passed at the
relay (path_token, header_token, hmac, ip). An alert authenticated by a URL alone
is plausible, not proven — declare the field and keep destructive automation off
such alerts until a playbook has confirmed them against the vendor.
field_mappings:
title: "jsonata: $exists(json.title) ? $string(json.title) : $exists(json.name) ? $string(json.name) : $exists(json.alert.name) ? $string(json.alert.name) : $exists(json.rule.name) ? $string(json.rule.name) : $exists(json.detection_name) ? $string(json.detection_name) : ('Webhook · ' & source)"
source: "jsonata: 'Webhook relay · ' & source"
description: "jsonata: body ? $substring(body, 0, 8000) : ('Binary body, ' & $string($length(body_b64)) & ' base64 characters')"
"custom:webhook_source": "source"
"custom:webhook_verified": "jsonata: $join(verified, ',')"
"custom:webhook_message_id": "id"
"custom:webhook_received_at": "received_at"
"custom:webhook_sender_ip": "remote_ip"
# Key/value rows of the decoded payload's top level, for a table field.
"custom:webhook_payload": "jsonata: json ? $toTable(json) : body"
ocsf:
- { source_path: "id", ocsf_field: "finding_info.uid" }
- { source_path: "id", ocsf_field: "metadata.uid" }
- { source_path: "received_at", ocsf_field: "time" }
- { source_path: "source", ocsf_field: "metadata.product.name" }