Notifications

Configure how and when you receive alerts when tripwires are triggered.

▶ Route trip alerts to Slack, Microsoft Teams or any HTTPS webhook.

Notification channels

Trip alerts are per user: each person chooses where their alerts go under Settings → Notifications. Alerts are sent the moment a tripwire is triggered.

Email

A detailed alert to your account address (or a separate notification address).

Slack

One-click Connect Slack (hosted), or paste an incoming-webhook URL.

Microsoft Teams

Paste the URL of a Teams Workflows “Post to a channel when a webhook request is received” flow. Alerts arrive as an Adaptive Card.

Custom HTTPS webhook

Signed JSON to your own endpoint — SOAR, ticketing, chat-ops, a Lambda. Hosted and self-hosted. See Custom webhooks.

Self-hosted deployments can additionally forward every detection to a SIEM/SOC pipeline — see SIEM / SOC forwarding. Admins can route alerts by tripwire tag to specific channels with a severity, via PUT /admin/notifications/routing.

Test before you rely on it

Every channel card has a Save & send test button. It saves your settings, sends a sample alert marked as a test to that channel — even before you switch it on — and shows the result inline: ✓ Delivered to https://soar.example.com/… in 182 ms, or the receiver’s own error, e.g. ✗ webhook returned HTTP 401: invalid token.

Below the cards, Recent deliveries lists every alert sent on your behalf with its status (delivered, pending retry, failed), the number of attempts and the last error, so you never need an admin to find out why an alert didn’t arrive.

Email notifications

Alert emails include the tripwire name, detection time, source IP, the credential that was tried, and a link to the tripwire in the console. Self-hosted email needs an SMTP relay (SMTP_HOST; see Configuration).

Slack

Self-hosted: the one-click Connect Slack button is hosted-only — the OAuth flow runs against our Slack app, whose redirect URL can’t cover every customer domain. Self-host installs use an incoming webhook instead: create one at Slack apps (Incoming Webhooks → Add New Webhook to Workspace) and paste the URL under Settings → Notifications. Same alerts, same delivery.

Custom webhooks

A custom webhook POSTs every detection, as signed JSON, to an HTTPS endpoint you own. It works the same on the hosted service and on self-hosted installs. Header values and signing secrets are encrypted at rest — with a dedicated AWS KMS key on the hosted service, and with NOTIFY_SECRET_KEY on self-host (without it, self-host deliveries are unsigned and header-less). On self-host an admin can turn the feature off under System Setup → Platform modules — doing so stops deliveries immediately, including queued retries.

Set up

  1. Settings → Notifications → Custom HTTPS endpoint: enter the URL (must be https://).
  2. Optionally add request headers (e.g. Authorization: Bearer …). Values are encrypted at rest and never shown again.
  3. Click Save & send test. A signing secret (whsec_…) is created on first save — Reveal it and configure your receiver to verify signatures.
  4. Switch the endpoint on.

The request

HeaderValue
Content-Typeapplication/json
User-AgentTripwire-Webhooks/1.0
X-Tripwire-Eventtripwire.triggered
X-Tripwire-DeliveryUUID of this delivery — identical on every retry. Use it as your idempotency key.
X-Tripwire-TimestampUnix seconds when this attempt was signed.
X-Tripwire-Signaturet=<unix>,v1=<hex HMAC-SHA256> — see Verifying signatures.
your headersAny you configured. They cannot override the X-Tripwire-* headers.

Payload (schema v1)

Every key is always present (empty string or {} when unknown) so you can rely on the shape; only severity is omitted when no routing rule assigned one. New fields may be added without notice; schema_version changes only on a breaking change.

{
  "event": "tripwire.triggered",
  "schema_version": "1",
  "delivery_id": "6f1c2a8e-4b3d-4e0a-9b7e-2d5f8c1a9e33",
  "test": false,
  "tripwire_id": "3b8e1f0c-7a2d-4c5e-8f9a-1b2c3d4e5f60",
  "tripwire_name": "prod-payroll-db",
  "tripwire_type": "postgresql",
  "tags": { "env": "prod", "owner": "data-platform" },
  "protocol": "postgresql",
  "source_ip": "203.0.113.7",
  "username": "tw_k3j9x2",
  "timestamp": "2026-10-07T09:14:03Z",
  "severity": "critical",
  "url": "https://tripwire.example.com/tripwire.html?id=3b8e1f0c-…",
  "trip_id": "trp_5f0c2b9a1e7d4c3b8a6f0e21",
  "namespace": "prod/db",
  "org_id": "org_cfddaba1-…",
  "user_agent": "",
  "detection": {
    "protocol": "postgresql",
    "source_ip": "203.0.113.7",
    "username": "tw_k3j9x2",
    "database": "payroll",
    "qname": "",
    "node_id": "sink-eu-west-2a",
    "edns_subnet": "",
    "timestamp": "2026-10-07T09:14:03Z"
  }
}
FieldMeaning
eventAlways tripwire.triggered.
delivery_idSame as X-Tripwire-Delivery; stable across retries.
testtrue only for Send test deliveries (tripwire id is all zeros, source IP is in TEST-NET-3). Real alerts are always false.
tripwire_id / tripwire_nameThe tripwire that was touched.
tripwire_typeThe technology, e.g. postgresql, aws, docx, web_token.
tagsThe tripwire’s tags — route on env, owner, instance-id…
protocol / source_ip / username / timestampThe original flat fields (kept for compatibility); duplicated in detection.
severityinfo | warning | critical when a tag-routing rule matched.
urlDeep link to the tripwire in the console.
trip_idThe detection’s stable id — the same trip_id GET /trips returns, so you can correlate a webhook with the API.
namespace / org_idWhere the tripwire lives (empty for the org root / a personal tripwire).
user_agentThe HTTP client’s User-Agent for web/document/QR beacons; empty for protocol honeypots and DNS.
detectionFull evidence: protocol, source IP, username tried, database, DNS query name, sink node, EDNS client subnet, timestamp (RFC 3339, UTC).

Verifying signatures

Compute HMAC-SHA256(secret, "<t>." + raw_body), hex-encode it, and compare it in constant time with each v1 value. Reject the request if t is more than 5 minutes from your clock — that blocks replays. Always verify against the raw request bytes, before any JSON parsing. More than one v1 may be present in future (secret-rotation overlap), so accept a match on any of them.

Node.js

const crypto = require('crypto');

// rawBody must be the exact bytes received (e.g. express.raw({ type: 'application/json' })).
function verifyTripwire(secret, header, rawBody, toleranceSec = 300) {
  let t = null;
  const sigs = [];
  for (const part of (header || '').split(',')) {
    const [k, v] = part.trim().split('=');
    if (k === 't') t = Number(v);
    if (k === 'v1') sigs.push(v);
  }
  if (!t || sigs.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay window
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  return sigs.some(s => {
    const got = Buffer.from(s, 'hex');
    return got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}

Python

import hashlib, hmac, time

def verify_tripwire(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    t, sigs = None, []
    for part in (header or "").split(","):
        k, _, v = part.strip().partition("=")
        if k == "t":
            t = int(v)
        elif k == "v1":
            sigs.append(v)
    if t is None or not sigs or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)

Go

func verifyTripwire(secret, header string, body []byte, tolerance time.Duration) bool {
	var ts int64
	var sigs []string
	for _, part := range strings.Split(header, ",") {
		k, v, _ := strings.Cut(strings.TrimSpace(part), "=")
		switch k {
		case "t":
			ts, _ = strconv.ParseInt(v, 10, 64)
		case "v1":
			sigs = append(sigs, v)
		}
	}
	if ts == 0 || len(sigs) == 0 || time.Since(time.Unix(ts, 0)).Abs() > tolerance {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	fmt.Fprintf(mac, "%d.", ts)
	mac.Write(body)
	want := mac.Sum(nil)
	for _, s := range sigs {
		if got, err := hex.DecodeString(s); err == nil && hmac.Equal(got, want) {
			return true
		}
	}
	return false
}
# Reproduce a signature by hand (bash + openssl):
TS=$(date +%s); BODY='{"hello":"world"}'
printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$WHSEC" -hex
# → compare with the v1= value of X-Tripwire-Signature: t=$TS,v1=…

Rotate the secret from the same card if it leaks; new deliveries are signed with the new secret immediately.

Delivery, retries and idempotency

  • Success is any 2xx response within 10 seconds. Respond fast and do the work asynchronously.
  • Redirects are not followed — a 3xx counts as a failure and the delivery log names the Location. Configure the final URL.
  • Retries: a failed delivery is retried after 30 s, 2 min, 8 min, 32 min and 1 h (6 attempts in total), then marked failed. Each retry is freshly signed and carries the same X-Tripwire-Delivery.
  • At-least-once: if your endpoint processed a request but the response was lost, you will see it again — de-duplicate on delivery_id.
  • The receiver’s status code and the first 200 bytes of its response body are shown in Recent deliveries and in the Send test result.

Network policy

Webhook URLs are user-supplied, so the server refuses to connect to loopback, link-local (including the 169.254.169.254 cloud metadata service) and multicast addresses — checked on the resolved IP at connect time, so DNS tricks cannot bypass it. Private ranges (RFC 1918, 100.64.0.0/10, fc00::/7) are refused by default; an operator alerting an internal SOAR sets WEBHOOK_ALLOW_PRIVATE_NETWORKS=true on the control server. Outbound proxies (HTTPS_PROXY) are honoured. Admin-configured SIEM forwarding is not subject to this policy.

Hosted service: webhook targets must be reachable on the public internet — private, loopback and cloud-metadata addresses are always refused. Deliveries come from AWS eu-west-2 with the Tripwire-Webhooks/1.0 user agent; verify the signature rather than allow-listing IPs.