Webhooks and SIEM Integration
Stream Zeuslock incident and policy events to your SIEM, chat, or on-call tooling with signed webhooks and ready-made connectors for Slack, Splunk, Sentinel, and PagerDuty.
What Zeuslock sends, and when
Zeuslock emits two families of events to external systems: incident events (something a user did that hit a policy) and policy events (something an operator changed in the console). Webhooks are HTTPS POST requests with a JSON body and an HMAC-SHA256 signature header. Every endpoint you configure receives only the event types you subscribe to, optionally filtered by severity, finding type, or user group.
The following event types are currently emitted:
incident.created— a new finding has just been raised by the extension, desktop agent, or CLI.incident.status_changed— an operator moved the incident between states (open, investigating, etc.).incident.resolved— the incident has been closed.incident.false_positive— an operator marked the finding as a false positive; useful for retraining your own dashboards.policy.changed— a detection or response policy was edited and published.policy.dry_run_completed— a dry-run backtest finished; payload includes the diff between current and proposed policy.
Configuring an endpoint
Open Operator Console → Settings → Webhooks → Add endpoint. You will be asked for:
- URL — must be HTTPS. Self-signed certificates are rejected.
- Secret — Zeuslock generates a random 32-byte hex string. Copy it now; it is shown once and stored only as a hash.
- Event types — tick the boxes for the events you want. We recommend at least
incident.createdandincident.status_changedfor SOC use. - Filters (optional) — restrict by
severity(low,medium,high,critical),finding_type(e.g.aws_access_key,credit_card), orgroup(any Okta/Azure AD group synced via SCIM).
Create one endpoint per downstream system. Mixing Slack and Splunk on the same URL makes retry analysis and rotation painful later.
Payload shape
Every event uses the same envelope: event, event_id, delivered_at, and a typed object whose key matches the event family (incident or policy). Example for incident.created:
{
"event": "incident.created",
"event_id": "evt_01HX9F4A1Y7QZ2C8K3J5W6N0PD",
"delivered_at": "2026-05-17T14:23:11Z",
"incident": {
"id": "inc_01HX9F4A1Y7QZ2C8K3J5W6N0PD",
"severity": "high",
"finding_type": "aws_access_key",
"user": {
"email": "alice@acme.com",
"group": "engineering"
},
"destination": "chat.openai.com",
"action": "block",
"redacted_preview": "Can you debug this AWS error? AKIA*** ..."
}
}
Field reference
| Field | Type | Example | Notes |
|---|---|---|---|
event | string | incident.created | Dotted event name. Stable across versions. |
event_id | string (ULID) | evt_01HX9F4A1Y... | Unique per delivery attempt group. Use for dedupe. |
delivered_at | string (RFC 3339) | 2026-05-17T14:23:11Z | UTC. May differ from incident creation time on retries. |
incident.id | string (ULID) | inc_01HX9F4A1Y... | Stable identifier. Same across all events for one incident. |
incident.severity | enum | high | One of low, medium, high, critical. |
incident.finding_type | string | aws_access_key | Matches the detector slug; see the detectors reference. |
incident.user.email | string | alice@acme.com | From SSO, never user-entered. |
incident.user.group | string | engineering | SCIM group name, if any. |
incident.destination | string | chat.openai.com | Hostname of the AI tool the prompt was headed for. |
incident.action | enum | block | One of monitor, anonymize, block. |
incident.redacted_preview | string | ... AKIA*** ... | Always redacted server-side. The raw secret never leaves the endpoint. |
Verifying the HMAC signature
Every request carries an X-Zeuslock-Signature header in the form sha256=<hex>. The digest is HMAC-SHA256(secret, raw_body), computed over the exact request body bytes — do not re-serialize. Reject anything that does not match, and reject anything older than five minutes by comparing delivered_at to your clock.
Python
import hmac, hashlib, time
from datetime import datetime, timezone
SECRET = b"your-32-byte-hex-secret"
def verify(raw_body: bytes, signature_header: str, delivered_at: str) -> bool:
if not signature_header.startswith("sha256="):
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
received = signature_header.split("=", 1)[1]
if not hmac.compare_digest(expected, received):
return False
sent = datetime.fromisoformat(delivered_at.replace("Z", "+00:00"))
return abs((datetime.now(timezone.utc) - sent).total_seconds()) < 300
Node.js
import crypto from "node:crypto";
const SECRET = "your-32-byte-hex-secret";
export function verify(rawBody, signatureHeader, deliveredAt) {
if (!signatureHeader?.startsWith("sha256=")) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(rawBody)
.digest("hex");
const received = signatureHeader.slice("sha256=".length);
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
const skew = Math.abs(Date.now() - Date.parse(deliveredAt));
return skew < 5 * 60 * 1000;
}
Go
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
"time"
)
var secret = []byte("your-32-byte-hex-secret")
func Verify(rawBody []byte, sigHeader, deliveredAt string) bool {
if !strings.HasPrefix(sigHeader, "sha256=") {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write(rawBody)
expected := mac.Sum(nil)
received, err := hex.DecodeString(strings.TrimPrefix(sigHeader, "sha256="))
if err != nil || !hmac.Equal(expected, received) {
return false
}
sent, err := time.Parse(time.RFC3339, deliveredAt)
if err != nil {
return false
}
return time.Since(sent).Abs() < 5*time.Minute
}
Use a constant-time comparison (hmac.compare_digest, crypto.timingSafeEqual, hmac.Equal). A naive == check leaks the secret over time through timing analysis.
Retries and idempotency
If your endpoint returns anything outside 2xx, or times out after 10 seconds, Zeuslock retries with the following backoff: 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours. After five consecutive failures the endpoint is automatically suspended, a banner appears in the Operator Console, and a Slack alert fires to your security channel if you have Slack connected.
Each redelivery reuses the same event_id. Treat that ID as a primary key in your downstream store and you will never double-count an incident, even during partial outages. The body and signature are byte-identical between retries.
Direct integrations
You do not need a custom receiver for the common targets. Each of the following lives under Settings → Integrations:
- Slack — OAuth install, pick a channel, optional severity filter. A common setup is sending only
highandcriticalto#security-alertsand the rest to#zeuslock-firehose. - Microsoft Teams — paste an incoming webhook URL. Use a dedicated channel; the connector posts adaptive cards that can be noisy in a general channel.
- PagerDuty — Events API v2 routing key. Severity maps directly:
criticalpages,highcreates a non-paging incident, lower severities are dropped. - Splunk HEC — HEC URL and token, sourcetype
zeuslock:incident. Events land as JSON; build dashboards offfinding_typeanduser.group. - Microsoft Sentinel — Log Analytics workspace ID and shared key. Custom log table is
Zeuslock_Incidents_CL. Hunting queries and an analytics rule template ship in the connector. - Generic webhook adapter — for everything else (Datadog, Elastic, Chronicle, in-house bus). Same signed envelope as above.
Testing your endpoint
Use the Send test event button next to each endpoint in the console. It fires a synthetic incident.created with event_id prefixed evt_test_ so you can filter it out of production dashboards.
If you prefer scripting it from CI, call the REST API with an operator API token:
curl -X POST \
-H "Authorization: Bearer $ZEUSLOCK_API_TOKEN" \
https://api.zeuslock.ai/v1/webhooks/wh_01HX.../test
Common failures: TLS handshake errors (check your certificate chain), 401 from your own auth proxy (allowlist the Zeuslock egress IPs documented in the console), and signature mismatches when a framework rewrites the request body. If your framework parses JSON before your handler runs, capture the raw bytes first or HMAC verification will fail.