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¶
- Under Signed webhook, enter your Endpoint URL (
https, port 443) and choose Save webhook. - untrust shows a signing secret once. Copy it into your receiver's secret store and choose I have copied it.
- 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:
- Compute the HMAC over the raw bytes of the body, before any JSON parsing.
- Compare it with
X-Untrust-Signaturein constant time. - 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.
- Route on the
eventfield of the verified body, never on theX-Untrust-Eventheader.
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
httpson port 443, at a public address. It does not follow redirects. - Each attempt times out after 5 seconds. Any
2xxresponse 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
analystoradministratorwith 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.