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:

  1. URL — must be HTTPS. Self-signed certificates are rejected.
  2. Secret — Zeuslock generates a random 32-byte hex string. Copy it now; it is shown once and stored only as a hash.
  3. Event types — tick the boxes for the events you want. We recommend at least incident.created and incident.status_changed for SOC use.
  4. Filters (optional) — restrict by severity (low, medium, high, critical), finding_type (e.g. aws_access_key, credit_card), or group (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

FieldTypeExampleNotes
eventstringincident.createdDotted event name. Stable across versions.
event_idstring (ULID)evt_01HX9F4A1Y...Unique per delivery attempt group. Use for dedupe.
delivered_atstring (RFC 3339)2026-05-17T14:23:11ZUTC. May differ from incident creation time on retries.
incident.idstring (ULID)inc_01HX9F4A1Y...Stable identifier. Same across all events for one incident.
incident.severityenumhighOne of low, medium, high, critical.
incident.finding_typestringaws_access_keyMatches the detector slug; see the detectors reference.
incident.user.emailstringalice@acme.comFrom SSO, never user-entered.
incident.user.groupstringengineeringSCIM group name, if any.
incident.destinationstringchat.openai.comHostname of the AI tool the prompt was headed for.
incident.actionenumblockOne of monitor, anonymize, block.
incident.redacted_previewstring... 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 high and critical to #security-alerts and 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: critical pages, high creates a non-paging incident, lower severities are dropped.
  • Splunk HEC — HEC URL and token, sourcetype zeuslock:incident. Events land as JSON; build dashboards off finding_type and user.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.