Skip to content

Alerts and webhooks

untrust can push events to your own systems as they happen: critical detections, blocked key releases and missed issue deadlines. Configure where they go under Connections › Integrations › Alert destinations.

Who: an organization administrator, signed in. API keys cannot configure destinations, whatever their role. A project admin can add destinations scoped to their own project.

Destinations

Destination How it is sent Signed
Signed webhook JSON over HTTPS to your own endpoint, one per organization Yes, HMAC-SHA256
Slack A message to a Slack incoming webhook (https://hooks.slack.com/... only) No
PagerDuty An Events API v2 trigger to your routing key No
Limit Value
Slack and PagerDuty destinations per organization 50
Destinations per project, added by a project admin 10
Destination name Up to 80 characters
PagerDuty routing key Up to 128 characters

A Slack URL or PagerDuty routing key is sealed when you save it and never shown again.

Events

Event Raised when
detection.critical A critical detection is ingested.
detection.high, .medium, .low, .info A detection of that severity is ingested. Opt-in: a destination receives these only when it names them.
release.blocked untrust's release gate denies a key release.
issue.sla_breached untrust records that an open issue has passed its SLA due time.
queue.post Someone chooses Post to Slack on a Live posture work-queue item.
alert.test You choose Test. Sent once, never retried.

For a Slack or PagerDuty destination, pick the events under Events; leave them all unchecked to receive every event except the opt-in lower-severity detections. The signed webhook always receives critical detections; turn on lower severities under Also send lower-severity detections.

A destination scoped to a project receives only that project's detections. A muted detection never alerts.

issue.sla_breached is a notification, not a timer: it can arrive some time after the due time passes. Do not rely on it as the only deadline control.

Set up the signed webhook

  1. Under Signed webhook, enter your Endpoint URL (https, port 443) and choose Save webhook.
  2. untrust shows a signing secret once. Copy it into your receiver's secret store and choose I have copied it.
  3. Choose Test. The result shows whether delivery succeeded and the HTTP status your endpoint returned.

To change the endpoint or replace the secret, enter a URL and choose Rotate and save. This generates a new secret, shown once, that replaces the old one. Pause and Resume stop and restart delivery without losing the configuration.

Receive a webhook

Each delivery is a POST with these headers:

Header Value
X-Untrust-Signature sha256=<hex>
X-Untrust-Timestamp Unix time in seconds, as a string
X-Untrust-Event The event name, as a routing hint only. It is not signed.
Content-Type application/json
User-Agent untrust-webhook/1

The body is compact JSON with sorted keys. A detection looks like this; fields with no value are left out:

{"detection":{"action":"...","catalogued":true,"id":"...","observed":"...","origin":"...","platform":"...","principal":"...","resource":"...","rule_id":"DET-01","severity":"critical","title":"..."},"event":"detection.critical","organization":"<org>"}

Every other event carries its own fields under data:

{"data":{"...":"..."},"event":"issue.sla_breached","organization":"<org>"}

For issue.sla_breached, data holds the issue's id, number, check_id, severity, owner, due and breached_at. It carries no evidence. The test event is {"data":{"id":"test","message":"untrust test alert"},"event":"alert.test","organization":"<org>"}.

Verify the signature

The signature is HMAC-SHA256, keyed with your signing secret, over the timestamp header, a dot, and the raw request body:

X-Untrust-Signature = "sha256=" + hex(HMAC-SHA256(secret, timestamp + "." + raw_body))

Before you trust a delivery:

  1. Compute the HMAC over the raw bytes of the body, before any JSON parsing.
  2. Compare it with X-Untrust-Signature in constant time.
  3. Reject a timestamp that is too old. untrust recommends a tolerance of 5 minutes; this is a recommendation for your receiver, not a value the service enforces.
  4. Route on the event field of the verified body, never on the X-Untrust-Event header.
import hashlib
import hmac
import json
import time

TOLERANCE_SECONDS = 300  # recommended: 5 minutes


def verify(secret: str, timestamp: str, signature: str, raw_body: bytes) -> dict:
    """Return the parsed body if the delivery is authentic and fresh, else raise ValueError."""
    if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        raise ValueError("stale timestamp")
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256)
    expected = "sha256=" + mac.hexdigest()
    if not hmac.compare_digest(expected, signature):
        raise ValueError("bad signature")
    return json.loads(raw_body)


# In your handler:
# body = verify(SECRET, request.headers["X-Untrust-Timestamp"],
#               request.headers["X-Untrust-Signature"], request.get_data())
# if body["event"] == "detection.critical": ...

Use the secret exactly as untrust showed it, as text. Respond with any 2xx status to acknowledge the delivery.

Delivery and retries

  • untrust delivers only to https on port 443, at a public address. It does not follow redirects.
  • Each attempt times out after 5 seconds. Any 2xx response counts as delivered.
  • A failed delivery is retried with exponential backoff, starting at 30 seconds and doubling each time, never more than 1 hour apart, for up to 6 attempts in all.
  • Each destination is retried independently, so one failing destination never delays another.

Slack messages

A Slack message has the text [untrust] <event>: <title>, a header, up to 10 fields (event, severity, rule, resource, principal, action, platform, origin, check, owner, issue number and due time, whichever apply) and the footer untrust · org <org>. Values are escaped so they cannot inject Slack formatting.

Post to Slack

On Live posture, Post to Slack sends one failing check from the work queue to every Slack destination subscribed to queue.post. The message is built by untrust from the stored result, not from text you type.

  • It needs analyst or administrator with organization-wide access.
  • Each person can post at most 5 times a minute; more are refused with HTTP 429.
  • Each post is recorded in the audit log as alert.posted.

PagerDuty events

untrust sends an Events API v2 trigger. Its dedup_key is a stable hash of the event and its id, issue number or resource, so repeats of the same event coalesce into one incident.

untrust severity PagerDuty severity
critical critical
high error
medium warning
low, info info

Audit

Configuring destinations and the webhook is recorded in the audit log as alert_destination.* and webhook.*. See Organizations, projects and roles.