feat(webhook-relay): map relayed messages into OCSF, not only custom fields (1.0.1)

The generic mapper now fills the OCSF fields a relayed message has a
place for: metadata.logged_time, log_provider and log_name, the checks
the delivery passed as metadata.labels (relay:hmac…), and raw_data. The
sender's address goes to unmapped.relay.sender_ip rather than
src_endpoint.ip: it is the vendor's cloud, and as an entity it would
correlate every alert of that vendor. metadata.product.name no longer
holds the source id.

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 15:48:27 +00:00
parent 6b76270cc6
commit 815db080f5
2 changed files with 30 additions and 3 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
id: webhook_relay id: webhook_relay
name: Webhook Relay name: Webhook Relay
version: 1.0.0 version: 1.0.1
description: "Reads the alerts that vendors pushed to riposte-relay, the public mailbox for alert webhooks (riposte/relay/). Riposte accepts no inbound connection, so a source whose only output is a webhook — Rapid7 InsightIDR Essential basic detection rules, for one — posts to the relay, and an alert rule on this integration reads one relay source back over outbound HTTPS. The relay knows no vendor: the rule chooses the client, the alert type and the mapper. A read consumes nothing; the rule's watermark is the read's `since`, so after a failover the standby reads the same mailbox from its own watermark." description: "Reads the alerts that vendors pushed to riposte-relay, the public mailbox for alert webhooks (riposte/relay/). Riposte accepts no inbound connection, so a source whose only output is a webhook — Rapid7 InsightIDR Essential basic detection rules, for one — posts to the relay, and an alert rule on this integration reads one relay source back over outbound HTTPS. The relay knows no vendor: the rule chooses the client, the alert type and the mapper. A read consumes nothing; the rule's watermark is the read's `since`, so after a failover the standby reads the same mailbox from its own watermark."
changelog: "1.0.0 — Initial release: fetch_messages (ingestion: one alert per relayed message, `since` watermark, page walk), relay_status, test_connection, and a generic mapper that keeps the raw body and the checks the message passed." changelog: "1.0.1 — The generic mapper fills the OCSF fields that have a place for a relayed message: metadata.logged_time (receipt), metadata.log_provider and log_name (relay, source), metadata.labels (the checks passed, relay:hmac…), raw_data (the payload). The sender address goes to unmapped.relay.sender_ip, not src_endpoint.ip: it is the vendor's cloud, and as an entity it would tie every alert of that vendor together. metadata.product.name no longer holds the source id. | 1.0.0 — Initial release: fetch_messages (ingestion: one alert per relayed message, `since` watermark, page walk), relay_status, test_connection, and a generic mapper that keeps the raw body and the checks the message passed."
category: ingestion category: ingestion
config_schema: config_schema:
@@ -13,6 +13,14 @@ description: |
falls back to the source. Severity is left to the alert rule's default: a generic falls back to the source. Severity is left to the alert rule's default: a generic
payload has no scale this mapper could trust. payload has no scale this mapper could trust.
The OCSF side carries what has a place in the schema: the relay's message id as the
finding uid, the receipt time (time and metadata.logged_time), the source as
metadata.log_name, the checks the delivery passed as metadata.labels (relay:hmac…),
the payload as raw_data. The sender's address goes to unmapped.relay.sender_ip and
NOT src_endpoint.ip: it is the vendor's cloud, and as an entity it would tie every
alert of that vendor together.
The custom fields repeat what layouts and automation conditions read most easily.
custom:webhook_verified is the trust level: the checks the delivery passed at the 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 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 is plausible, not proven — declare the field and keep destructive automation off
@@ -29,7 +37,26 @@ field_mappings:
# Key/value rows of the decoded payload's top level, for a table field. # Key/value rows of the decoded payload's top level, for a table field.
"custom:webhook_payload": "jsonata: json ? $toTable(json) : body" "custom:webhook_payload": "jsonata: json ? $toTable(json) : body"
ocsf: ocsf:
# ── Finding ───────────────────────────────────────────────────────
# The relay's message id: stable for one delivery. A vendor mapper replaces it with
# the vendor's own alert id.
- { source_path: "id", ocsf_field: "finding_info.uid" } - { source_path: "id", ocsf_field: "finding_info.uid" }
- { source_path: "id", ocsf_field: "metadata.uid" } - { source_path: "id", ocsf_field: "metadata.uid" }
# When the relay received it — the best time a payload of unknown shape offers.
# A vendor mapper puts the detection's own time in `time` and keeps this one.
- { source_path: "received_at", ocsf_field: "time" } - { source_path: "received_at", ocsf_field: "time" }
- { source_path: "source", ocsf_field: "metadata.product.name" } - { source_path: "received_at", ocsf_field: "metadata.logged_time" }
# ── Where it came from ────────────────────────────────────────────
- { source_path: "'riposte-relay'", ocsf_field: "metadata.log_provider" }
- { source_path: "source", ocsf_field: "metadata.log_name" }
# The checks the delivery passed, as labels: relay:path_token, relay:hmac, relay:ip.
# Kept an array even with one check (the singleton trap).
- { source_path: "[verified.('relay:' & $)]", ocsf_field: "metadata.labels" }
# The payload exactly as sent: what an analyst and the case synthesis read when no
# vendor mapper exists yet.
- { source_path: "body", ocsf_field: "raw_data" }
# The sender's address is the VENDOR's (its cloud, its collector), never the
# attacker's or the victim's: as src_endpoint.ip it would become an entity, and
# every alert of that vendor would correlate on it. It stays out of the schema.
- { source_path: "remote_ip", ocsf_field: "unmapped.relay.sender_ip" }
- { source_path: "content_type", ocsf_field: "unmapped.relay.content_type" }