Safeguard publishes its state changes as named events, such as findings.vulnerability.created, scan.completed, remediation.pr_opened, policy.violation and audit.action, and delivers them as signed HTTP POST requests to endpoints you register on the Webhooks page. Each delivery carries an X-Safeguard-Signature header containing an HMAC-SHA256 over a timestamp and the raw body, so your receiver can prove Safeguard sent it, and delivery is at least once with exponential backoff and a dead-letter queue, so your receiver deduplicates by event ID. With those three pieces in place (verify, deduplicate, acknowledge fast), the same event stream can feed a ticketing system, a chat channel, an on-call tool and a SIEM without polling.
This post is for the security platform owner or integration engineer who has been asked to "get Safeguard into our systems," and for the SOC lead who will consume what arrives. It covers every outbound path Safeguard documents (platform webhooks, cloud streams, Guard webhooks, chat integrations and workflow steps), the event catalog domain by domain, the payload and headers, signature verification in Python, Node.js and Go, what the retry schedule means for your receiver, how to test without waiting for a real finding, and a staged rollout. Everything about Safeguard here comes from Safeguard's documentation at docs.safeguard.sh. Where we add a design recommendation of our own, we label it as a suggestion.
Key takeaways
- Events are named
<domain>.<subject>.<verb>. Domains include findings, scans, remediation, attestations, workflows, policy, assets and vendors, and audit. You subscribe to the event types you need and can filter server side with a subset of Safeguard's policy expression language. - Every delivery is signed. The header is
X-Safeguard-Signature: v1,t=<unix>,s=<hex hmac sha-256>, computed over the timestamp, a literal dot and the raw request body with your subscription's signing secret. Verify it with a constant-time comparison and reject stale timestamps. - Delivery is at least once, not exactly once. Safeguard retries with exponential backoff over 24 hours and parks undelivered events in a dead-letter queue you can replay from the UI. Your consumer must deduplicate on
X-Safeguard-Event-Id. - Order is guaranteed only narrowly. Within one event type,
created_atincreases monotonically for a given subject (a finding, an asset, a workflow run). Use it to discard stale updates; do not assume order across types. - Guard has its own webhook system with a different header layout, one event name (
alert.triggered) and a short retry schedule. Do not reuse verification code between the two. - Chat integrations and workflow steps are the no-code paths. Slack, Microsoft Teams and Discord integrations post to a default channel you pick at setup; workflows can create Jira or Linear issues, page on-call and notify chat as steps.
- There is no language SDK and no CLI command for subscriptions. Subscriptions are managed on the Webhooks page, and the verification code in this post is all a receiver needs.
Why event plumbing comes before scale
A security platform that people have to log in to and look at works for a pilot. It stops working when the program spans hundreds of repositories, several business units and a SOC that lives in its own tools. At that point the question changes from "what does Safeguard show?" to "how does what Safeguard knows reach the systems where work is assigned, tracked and audited?"
Pulling on a schedule is the usual first answer, and it has known costs. Every poll asks "anything new?" and most answers are "no." Changes land in your systems up to one polling interval late. And the polling job becomes another piece of glue someone owns, with its own credentials and failure modes. Push delivery inverts this: the source tells you when something changed, signed so you can verify who sent it.
NIST's guidance points the same way. SP 800-92, the Guide to Computer Security Log Management (September 2006), treats centralised, reliable collection of security-relevant records as a basic operational need, not an add-on. SP 800-61 Revision 3 (April 2025) frames incident response as part of cybersecurity risk management under CSF 2.0, which only works if detection signals reach the people and tools that respond. A webhook stream is how a supply chain security platform feeds both: the SIEM gets a durable record, and the ticketing and on-call systems get work items.
Which outbound path fits which job?
Safeguard documents several ways for events to leave the platform. They are not interchangeable, so pick per job.
| Path | What it carries | Where it is configured | Best for |
|---|---|---|---|
| Platform webhooks (Webhooks and Events) | The full event catalog: findings, scans, remediation, attestations, workflows, policy, assets, vendors, audit | Webhooks page: endpoint URL, event types, signing secret, optional filter | Custom integrations, SIEM ingestion, ticket sync, data lakes |
| Cloud streams | The same events, pushed into a stream | Delivery options on the Webhooks and Events page | Teams already standardised on a managed stream |
| Guard webhooks | Guard alert firings (alert.triggered) | Per organization, alongside Guard policies and alert rules | AI agent and MCP traffic alerts |
| Slack, Teams and Discord integrations | Findings and platform events as chat messages | Settings, Integrations | Human awareness in a chat channel, no code |
| Workflow steps | Notifications, tickets and pages as steps in a workflow | Workflows, as YAML in Git or in the UI | Conditional, multi-step responses with an audit trail |
Two points decide most designs. First, if you need to verify the sender, deduplicate and route by your own rules, you want platform webhooks into a receiver you control. Second, if you want a response that depends on conditions (only reachable, only on main, only KEV), a workflow does that inside Safeguard and produces signed audit records for each side effect. Many teams run both: workflows for the opinionated responses, webhooks for the SIEM and the system of record.
The event catalog, domain by domain
Safeguard's Webhooks and Events page lists the catalog. Every name follows <domain>.<subject>.<verb>, which makes routing by prefix easy: a receiver can send everything under findings. to one handler and everything under remediation. to another. The tables below cover the domains most integrations use; the docs page is the full reference.
Findings
| Event | When it fires |
|---|---|
findings.vulnerability.created | A new vulnerability finding on any asset |
findings.vulnerability.severity_changed | An EPSS, KEV or CVSS update changed the severity bucket |
findings.vulnerability.reachability_changed | The reachability classification was updated |
findings.vulnerability.suppressed | An exception was granted or a false positive confirmed |
findings.vulnerability.resolved | An upgrade, patch or removal was verified |
findings.license.created | A new license policy violation |
The vulnerability events form a lifecycle, and that matters for ticket sync. created opens the work, severity_changed and reachability_changed change its priority, suppressed records a decision not to fix, and resolved closes it with verification. A ticket integration that only listens to created will leave stale tickets open forever, and one that only listens to resolved will never see the decision trail. Subscribe to the full set.
severity_changed deserves attention. Severity moves when outside data moves: a CVE added to CISA's Known Exploited Vulnerabilities catalog, an EPSS score update or a CVSS rescoring. Nothing in your code changed, but the finding's urgency did. This is exactly the change polling misses and on-call wants to hear about.
Scans
| Event | When it fires |
|---|---|
scan.started | A scan of any asset begins |
scan.completed | A scan completes; the payload includes a summary of new findings |
scan.failed | A scan errored; the payload includes the failure reason |
scan.failed is the one teams forget: a scan that silently stops running looks like a clean bill of health. Route it to whoever owns the integration.
Remediation
| Event | When it fires |
|---|---|
remediation.plan_created | Griffin, Safeguard's security model, generated a remediation plan |
remediation.pr_opened | An auto-fix pull request was opened in your repository |
remediation.pr_merged | The auto-fix pull request was merged |
remediation.pr_closed | The auto-fix pull request was closed without merging |
remediation.self_heal.started | A self-heal container rebuild started |
remediation.self_heal.promoted | A self-heal image was promoted to a registry tag |
remediation.self_heal.rolled_back | A self-heal rollback was triggered |
These events let you measure your remediation flow in your own tools. remediation.pr_closed is worth a human look every time, because a closed fix usually means a compatibility problem or a disagreement about priority.
Attestations, workflows and policy
| Event | When it fires |
|---|---|
attestation.created | A new attestation was published |
attestation.verification_failed | Attestation verification failed (admission, CI and similar) |
workflow.run.started | A workflow run began |
workflow.run.completed | A workflow run finished successfully |
workflow.run.failed | A workflow run failed |
policy.violation | A BLOCK or WARN fired |
policy.exception_granted | An exception was approved |
policy.exception_expired | A time-boxed exception expired |
policy.violation is the bridge between enforcement and response. Safeguard's guardrails evaluate rules against a document that combines SBOM, vulnerability, license, signature, provenance and reachability data, with effects of BLOCK, WARN or AUTO_FIX, according to the Guardrails and Enforcement docs. When a BLOCK stops a deployment, the team that owns the service needs to know within minutes, and the event is how they find out.
policy.exception_expired is the event most programs need and few build for: when a time-boxed exception lapses, the violation is live again, and if nobody is told the first sign is a blocked release.
Assets, vendors and audit
| Event | When it fires |
|---|---|
asset.discovered | A new asset was found (repository, image, model, workload, SBOM) |
asset.ungoverned | An asset was flagged as missing an owner, an SBOM or governance |
vendor.sbom_received | A new vendor SBOM was uploaded |
vendor.risk_score_changed | A third party risk score crossed its threshold |
audit.action | Any state-mutating action recorded in the audit log |
audit.action lets your SOC correlate "who granted that exception" with the rest of its identity data. asset.ungoverned turns "we have repositories nobody owns" from a quarterly spreadsheet into a queue.
A routing map you can start from
The table below is our suggestion for a first routing map, not a product default. Adjust it to your teams.
| Event (prefix or name) | Destination | Action (suggested) |
|---|---|---|
findings.vulnerability.created (filtered to critical and high in production) | Ticketing | Create an issue for the owning team |
findings.vulnerability.severity_changed | Ticketing, on-call if KEV | Update priority; page if it moved to KEV in production |
findings.vulnerability.resolved | Ticketing | Close the issue with the verification note |
findings.vulnerability.suppressed | Ticketing, SIEM | Comment with the decision; keep the record |
scan.failed | Platform team channel | Alert the integration owner |
remediation.pr_opened, remediation.pr_closed | Team chat | Ask for review; flag closed-unmerged fixes |
policy.violation | Team chat, on-call for BLOCK in production | Notify; page on production BLOCK |
policy.exception_expired | Ticketing | Reopen the underlying finding's issue |
asset.ungoverned | Platform backlog | Assign an owner |
audit.action, policy.*, attestation.verification_failed | SIEM | Store and correlate |
workflow.run.failed | Platform team channel | Investigate the automation |
What a delivery looks like
According to the Webhooks and Events docs, a delivery is a JSON body with a stable envelope:
{
"id": "evt_01J...ABC",
"type": "findings.vulnerability.created",
"created_at": "2026-04-23T12:34:56.789Z",
"data": { "finding": { ... }, "asset": { ... } },
"tenant": "acme",
"api_version": "2026-01-01"
}
Read each field for what it is for:
idis stable per event, so a redelivery of the same event carries the same value. It is your idempotency key.typeis the catalog name. Route on it.created_atis an RFC 3339 style UTC timestamp with milliseconds. Within one event type and one subject, it increases monotonically, which is how you tell a newer state from an older one when deliveries arrive out of order.dataholds the objects the event is about, scoped to the event (for a finding event, the finding and the asset).tenantidentifies your Safeguard tenant.api_versionidentifies the payload schema. Log it and alert if it changes.
Every delivery also carries four headers:
| Header | Meaning |
|---|---|
X-Safeguard-Signature | v1,t=<unix>,s=<hex hmac sha-256>: scheme version, signing timestamp and signature |
X-Safeguard-Event-Id | The event ID, for idempotency |
X-Safeguard-Delivery-Attempt | The attempt count, 1 for the first delivery |
X-Safeguard-Api-Version | The payload API version |
The test events Safeguard can generate carry "test": true in the payload, so your production pipeline can drop them after verifying them.
One note on documentation: the API Reference page carries a short webhook summary with a simpler event list and an older payload example. It points to the Webhooks and Events page "for signing, replay protection and the full event list," so treat that page as the reference. Our suggestion is to make your parser route on the type field, log any delivery whose shape it does not recognise, and park it rather than drop it.
Creating a subscription
Subscriptions are created on the Webhooks page in the Safeguard web app. The docs list four inputs: the endpoint URL, the event types, a signing secret and an optional filter. Subscriptions are listed, edited and deleted on the same page. There is no CLI command for creating or managing subscriptions, so if you keep integration configuration in code, record the subscription settings in your repository as documentation and apply them in the UI.
A few setup choices we suggest:
- One subscription per consumer, not one per event type. A SIEM receiver that wants all
audit.actionandpolicy.*events should have its own subscription with its own secret. A ticketing bridge gets another. Separate secrets mean a leak in one bridge does not let anyone forge events to the other. - Generate the signing secret with a cryptographic random source and store it in your secret manager, not in the receiver's configuration file. RFC 2104 notes that HMAC keys shorter than the hash output length are "strongly discouraged," which for SHA-256 means at least 32 random bytes.
- Start in staging. Point a subscription at a staging receiver first, send test events, and only then create the production subscription.
Server-side filters
Every subscription can be filtered on Safeguard's side using a subset of the policy expression language. The docs show this example:
severity in ["critical","high"]
AND env == "production"
AND finding.kev == true
Filtering at the source cuts traffic, but events you filtered out never arrive, so they are not in your SIEM either. Our suggestion: filter hard for human-facing destinations (tickets, on-call, chat) and lightly or not at all for the SIEM subscription, where completeness matters more than volume.
Verifying signatures
Without a signature check, anyone who learns your endpoint URL can post a fake findings.vulnerability.resolved and close a real ticket.
How the signature is built
The Webhooks and Events docs specify the construction:
- Safeguard takes the Unix timestamp
tat signing time. - It builds the signed message as the timestamp, a literal
., then the raw request body bytes. - It computes HMAC-SHA256 over that message using your subscription's signing secret as the key, and hex encodes it as
s. - It sends
X-Safeguard-Signature: v1,t=<t>,s=<s>.
HMAC is defined in RFC 2104 (February 1997) and standardised by NIST in FIPS 198-1 (July 2008). It combines a secret key with a hash function so that only someone holding the key can produce a valid tag for a message. Including the timestamp inside the signed message is what makes a replay guard possible: an attacker who captured an old delivery cannot change its timestamp without breaking the signature.
The four rules of a correct check
- Use the raw body bytes. Verify against exactly what arrived on the wire, before any JSON parsing. RFC 8259 lets JSON writers vary whitespace and member order, so a parsed and re-serialised body is not guaranteed to be byte-identical to what was signed. Many web frameworks parse JSON by default; turn that off for the webhook route.
- Compare in constant time. Python's documentation for
hmac.compare_digestsays it avoids "content-based short circuiting behaviour, making it appropriate for cryptography." Node.js'scrypto.timingSafeEqualdoes the same, with a catch covered below. A plain==can leak how many leading characters matched. - Reject stale timestamps. The docs' sample code rejects events whose timestamp is more than 300 seconds from the receiver's clock. Keep your receiver's clock synchronised, or legitimate deliveries will fail this check.
- Check the scheme version. The header starts with
v1. Our suggestion is to reject any other version explicitly, so a future scheme change produces a clear error rather than a silent mismatch.
Python
The docs' Python sample, made a little more defensive. The hardening (version check, missing-field handling, header length cap) is our suggestion.
import hmac
import hashlib
import time
MAX_SKEW_SECONDS = 300 # matches the replay guard in Safeguard's sample
def verify_safeguard(secret: bytes, header: str, body: bytes, now: float | None = None) -> bool:
if not header or len(header) > 512:
return False
version, _, rest = header.partition(",")
if version != "v1":
return False
fields = {}
for part in rest.split(","):
key, sep, value = part.partition("=")
if sep:
fields[key.strip()] = value.strip()
ts, sig = fields.get("t"), fields.get("s")
if not ts or not sig or not ts.isdigit():
return False
now = time.time() if now is None else now
if abs(now - int(ts)) > MAX_SKEW_SECONDS:
return False
expected = hmac.new(secret, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
In Flask, read the body with request.get_data() before touching request.json.
Node.js
Node.js documents that crypto.timingSafeEqual throws an error if the two buffers have different byte lengths. A forged header with a short signature would therefore crash a naive handler instead of returning 401. Check the length first:
import { createHmac, timingSafeEqual } from "node:crypto";
const MAX_SKEW_SECONDS = 300;
export function verifySafeguard(secret, header, rawBody, nowSeconds = Date.now() / 1000) {
if (typeof header !== "string" || header.length > 512) return false;
const [version, ...rest] = header.split(",");
if (version !== "v1") return false;
const fields = Object.fromEntries(
rest.map((p) => p.split("=", 2)).filter((kv) => kv.length === 2)
);
const ts = fields.t;
const sig = fields.s;
if (!ts || !sig || !/^\d+$/.test(ts)) return false;
if (Math.abs(nowSeconds - Number(ts)) > MAX_SKEW_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(sig, "utf8");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
With Express, mount the webhook route with a raw body parser so req.body is a Buffer:
import express from "express";
const app = express();
app.post("/hooks/safeguard", express.raw({ type: "*/*", limit: "1mb" }), async (req, res) => {
const ok = verifySafeguard(process.env.SG_WEBHOOK_SECRET, req.get("X-Safeguard-Signature"), req.body);
if (!ok) return res.status(401).end();
await enqueue(req.get("X-Safeguard-Event-Id"), req.body); // durable queue, see below
res.status(202).end();
});
The Node.js docs add a warning worth repeating: using timingSafeEqual "does not guarantee that the surrounding code is timing-safe." Do not branch on partial matches or log the expected signature.
Go
In Go, hmac.Equal compares MACs in constant time.
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"math"
"strconv"
"strings"
"time"
)
const maxSkew = 300 // seconds
func VerifySafeguard(secret []byte, header string, body []byte) bool {
if len(header) == 0 || len(header) > 512 {
return false
}
parts := strings.Split(header, ",")
if parts[0] != "v1" {
return false
}
fields := map[string]string{}
for _, p := range parts[1:] {
if k, v, ok := strings.Cut(p, "="); ok {
fields[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
}
ts, err := strconv.ParseInt(fields["t"], 10, 64)
if err != nil || fields["s"] == "" {
return false
}
if math.Abs(float64(time.Now().Unix()-ts)) > maxSkew {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(fields["t"] + "."))
mac.Write(body)
expected := []byte(hex.EncodeToString(mac.Sum(nil)))
return hmac.Equal(expected, []byte(fields["s"]))
}
Read the body with io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) so an oversized request cannot exhaust memory, then verify, then parse.
Testing your verifier before a real event arrives
The docs describe synthetic test events that do not change any real state and carry "test": true. Use them once your receiver is reachable. The docs do not show where in the UI to trigger them, so check the Webhooks page in your tenant. Before that, you can exercise the verifier locally by signing a sample payload yourself with the same construction. This is our suggestion, adapted from the approach Safeguard's Guard webhook docs show for Guard:
SECRET="$(cat ./staging-webhook-secret)"
BODY='{"id":"evt_local_test_1","type":"scan.completed","created_at":"2026-11-04T09:00:00.000Z","data":{},"test":true}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -i -X POST http://localhost:8080/hooks/safeguard \
-H "Content-Type: application/json" \
-H "X-Safeguard-Signature: v1,t=$TS,s=$SIG" \
-H "X-Safeguard-Event-Id: evt_local_test_1" \
-H "X-Safeguard-Delivery-Attempt: 1" \
-d "$BODY"
Then run the negative cases: an altered body, a timestamp ten minutes old, a v2, prefix and a truncated signature should each return 401, never a crash. A self-signed request proves your verifier works, not that the subscription is configured correctly; Safeguard's test events cover that.
Rotating the signing secret
The Webhooks and Events docs say a subscription has a signing secret, but do not describe a dual-secret rotation window. Safeguard's Guard webhook docs, for the separate Guard system, say a rotated secret invalidates verification immediately with no grace period. Plan for the same behaviour unless the docs say otherwise. A pattern we suggest for zero-downtime rotation on platform webhooks:
- Create a second subscription to the same receiver with the same event types and filter, and a new secret.
- Teach the receiver to accept either secret (try the new one, then the old one).
- Confirm deliveries verify with the new secret on the Webhooks page.
- Delete the old subscription, then remove the old secret from the receiver.
During the overlap the receiver gets each event twice. That is harmless, because the receiver already deduplicates by event ID.
Retries, duplicates and ordering
The reliability section of the Webhooks and Events docs is short and every line of it shapes your receiver.
What Safeguard does
- At-least-once delivery. "Consumers must handle duplicates by X-Safeguard-Event-Id."
- Exponential backoff. The docs describe 11 attempts over 24 hours and list the steps as 1s, 5s, 30s, 1m, 5m, 15m, 1h, 3h, 6h, 12h and 24h.
- A dead-letter queue. Events that are never delivered land in a DLQ that you can replay from the UI.
- Per-endpoint observability. The Webhooks page shows delivery attempts (success, retry, failed), average latency, DLQ size, and recent deliveries with request and response bodies, scrubbed of secrets.
What that means for your receiver
Expect duplicates and make them harmless. At-least-once is the normal contract for event delivery, not a Safeguard quirk. The CloudEvents specification, a CNCF project, says producers must make source plus id unique for each distinct event and that a re-sent duplicate may carry the same id. The Amazon Kinesis Data Streams developer guide tells applications to "anticipate and appropriately handle processing individual records multiple times." Google Cloud Pub/Sub offers an exactly-once feature precisely because its default delivery can redeliver. The fix is the same everywhere: record each processed event ID and skip repeats.
Keep dedupe records longer than the retry window. Read literally, the listed backoff steps add up to more than a day if each is a gap between attempts. Our suggestion is to keep processed event IDs for at least several days, and for as long as you might replay from the DLQ. A table keyed on the event ID with a timestamp column and a scheduled cleanup is enough.
Acknowledge fast, process later. The general webhook docs do not state a per-attempt timeout or which status codes count as success. Guard's separate webhook system treats any status below 400 as delivered and times out each attempt after 15 seconds. Our suggestion for platform webhooks is to verify the signature, write the event to a durable queue, return a 2xx immediately, and do the ticketing or SIEM call from a worker. A receiver that calls a slow ticketing API inline turns a slow dependency into retries, and retries into duplicates.
Return honest status codes. 401 for a bad signature, 2xx once queued, 5xx if your queue is down so the event is retried. RFC 9110 defines Retry-After and RFC 6585 defines 429 Too Many Requests, but Safeguard's docs do not say whether its sender reads them, so do not rely on them.
Use the attempt header for monitoring, not logic. X-Safeguard-Delivery-Attempt greater than 1 means an earlier attempt failed or timed out. Count those in your metrics. A rising rate of second attempts is an early warning that your receiver is slow, well before anything lands in the DLQ.
Respect the narrow ordering guarantee. Within one event type, created_at increases monotonically for a given subject. That means two findings.vulnerability.severity_changed events for the same finding can be ordered by created_at, even if a retry delivers the older one second. Store the last created_at you applied per subject and event type, and ignore anything older. Across different types there is no ordering guarantee, so a resolved can arrive before the severity_changed that preceded it. Our suggestion is to make the resolved handler win, so a resolved finding is not reopened by a late severity update.
A dedupe and ordering sketch
CREATE TABLE sg_processed_events (
event_id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE sg_subject_state (
subject_key TEXT NOT NULL, -- for example the finding id from data
event_type TEXT NOT NULL,
last_created TIMESTAMPTZ NOT NULL,
PRIMARY KEY (subject_key, event_type)
);
The worker inserts into sg_processed_events with ON CONFLICT DO NOTHING and stops if no row was inserted. It then upserts sg_subject_state only when the incoming created_at is newer, and applies the change only in that case. Both tables are our suggestion; field names inside data should come from the payloads you actually receive, not from this post.
Draining the dead-letter queue
After an outage on your side, fix the receiver, check DLQ size on the Webhooks page and replay from the UI. Because the receiver deduplicates and orders per subject, an overlapping replay is safe. Our suggestion: alert whenever the DLQ is not empty.
Streams instead of HTTP
Some teams would rather consume a stream than run an HTTP receiver. The Webhooks and Events docs list these delivery options:
| Target | How the docs describe it |
|---|---|
| AWS Kinesis Data Streams or Firehose | Direct integration |
| Azure Event Hubs | Direct integration |
| Google Cloud Pub/Sub | Direct integration |
| Confluent Cloud or Apache Kafka | Via webhook sink |
| NATS | Via webhook sink |
| Redis Streams | Via webhook sink |
"Via webhook sink" means the events still arrive as HTTP webhooks, at a sink component that writes them into your stream. Whichever path you use, the delivery semantics of the stream are added to Safeguard's. Kinesis documents duplicates from both producer and consumer retries, Pub/Sub redelivers unless exactly-once is enabled on the subscription, and Event Hubs keeps events for a time-based retention period so consumers can re-read. So the dedupe-by-event-ID rule applies at the consumer of the stream too.
Our suggestion for a webhook sink: verify the signature in the sink, then write the raw body plus the headers you need (X-Safeguard-Event-Id, X-Safeguard-Api-Version) as the record, keyed on the event ID. Downstream consumers then never need the signing secret, and anything in the stream is known to be verified.
Guard webhooks are a separate system
Safeguard Guard, which applies your policy to MCP server and AI agent traffic, has its own webhook system for alert firings. The Guard Webhooks docs are explicit: the two systems have "different signature schemes, retry schedules, and event catalogs," and verification code written for one should not be reused for the other.
What Guard sends
Guard webhooks fire when an alert rule matches. Alert rules watch signals such as a jailbreak score, an anomaly score, the blocked-request ratio, server reachability and an overall request risk score. Today these firings use one event name, alert.triggered, and a webhook can subscribe to it or to *. Each registered endpoint has a name, URL, secret, event list and an active flag, and records last_delivered_at and failure_count. The secret is write-only: it is never shown again after you set it, so if you lose it you rotate it.
The body carries event and a data object with the alert's ID, the full rule that matched, a severity, a human-readable message and triggered_at. The docs say not to expect per-request detail such as the agent or server ID in the payload; correlate with Guard's Audit Trail and Alerts by time and signal instead.
How Guard signs and retries
| Guard webhooks | Platform webhooks | |
|---|---|---|
| Signature header | X-Safeguard-Signature: sha256=<hex> plus a separate X-Safeguard-Timestamp | One header, X-Safeguard-Signature: v1,t=<unix>,s=<hex> |
| Signed message | <timestamp>. followed by the raw body | <t>. followed by the raw body |
| Event header | X-Safeguard-Event | X-Safeguard-Event-Id (plus attempt and API version headers) |
| Success | Any status below 400; 15 second timeout per attempt | Not stated in the docs; we suggest a fast 2xx |
| Retries | Four attempts in total: the original plus retries after 1s, 5s and 30s, then abandoned | Exponential backoff over 24 hours, then the DLQ |
| Idempotency key | data.id | X-Safeguard-Event-Id |
| Durable record | Audit Trail and Alerts | DLQ replay, plus your own store |
The short retry schedule is the important difference. If your Guard receiver is down for a minute, those alerts are not redelivered later. The Guard docs say to treat the webhook as a low-latency trigger and the Audit Trail as the durable log. Our suggestion: if Guard alerts must reach your SIEM completely, also export from the audit trail on a schedule, and use the webhook only for paging.
Guard has no synthetic test event; its docs show signing a sample payload with openssl, then triggering a real low-threshold rule to prove the full path.
Chat integrations: the no-code path
If the goal is "the team sees it in chat," you may not need a receiver at all. Safeguard's Slack, Microsoft Teams and Discord integrations are configured under Settings, Integrations, in the ESCM, Portal and TPRM products.
| Platform | Authentication options | Where messages go |
|---|---|---|
| Slack | Bot Token (xoxb-...) or Incoming Webhook | One default channel picked at setup (bot) or the webhook's bound channel |
| Microsoft Teams | Incoming Webhook or Bot Framework (tenant ID, app ID, app secret) | One default team and channel (Bot Framework) or the webhook's bound channel |
| Discord | Bot Token or Webhook | One default channel in a chosen server (bot) or the webhook's bound channel |
Credentials are validated before any channel picker appears, and bad credentials stop the flow with an inline error. The most common snag, per the docs, is a Slack bot that has not been invited to any channel, so the picker is empty.
Every chat integration sends to its single default channel. Teams is notification-only. If you need "criticals to the on-call channel, everything else to the team channel," the docs point to filtering upstream through webhook filters. In practice that means either several chat integrations fed by workflows with conditions, or a platform webhook into your own small router. Our earlier post on Safeguard alerts in Slack, Microsoft Teams and Discord covers chat setup and noise control in depth; this post stays on the event plumbing underneath.
Workflows: Safeguard's own consumer of these events
The Webhooks and Events docs describe Workflows as "Safeguard's own consumer of these events." A workflow is a YAML definition with a trigger, ordered steps, step-level if conditions and a concurrency limit. It lives in a Git repository or the Safeguard UI and runs on Safeguard's managed cloud runner or a self-hosted runner inside your network.
Workflow triggers include on.vulnerability.published, on.policy.violation, on.image.pushed, on.repo.pushed, on.sbom.received, on.vendor.sbom_updated, on.schedule (cron in UTC) and on.manual. The built-in step library includes notification steps (slack-notify, teams-notify, email-owners, pagerduty-trigger) and ticket steps (jira-create, jira-comment, linear-create). This is the docs' own example of a response to a blocked production admission:
This is a Safeguard Workflows definition. It runs in Safeguard's runner, not in GitHub Actions.
apiVersion: safeguard.sh/workflows/v1
kind: Workflow
metadata:
name: prod-admission-gate
spec:
on:
policy.violation:
filters:
policy: prod-admission-policy
decision: BLOCK
steps:
- id: open-ticket
uses: safeguard/jira-create@v2
with:
project: SEC
summary: "Blocked production admission: ${{ trigger.image }}"
severity: ${{ trigger.severity }}
- id: page-oncall
uses: safeguard/pagerduty-trigger@v1
with:
service: prod-admission-failures
if: ${{ trigger.severity == 'critical' }}
Every run produces a step timeline, structured logs, OpenTelemetry spans to your configured OTLP endpoint, and audit records signed by Safeguard for every external side effect, such as a ticket created or a notification sent. Workflow runs also emit workflow.run.started, workflow.run.completed and workflow.run.failed as platform events, so your SIEM can watch the automation itself.
When should you use a workflow and when a webhook? Our rule of thumb: if the response is a Safeguard action followed by a notification (scan, check reachability, open a fix pull request, tell the team), use a workflow. If the destination is a system Safeguard does not have a step for, or you need your own enrichment, ordering and storage, use a webhook. The Workflows documentation covers triggers, steps, runners and promotion between environments in full.
Pull when you need to ask a question
Webhooks answer "tell me when something changes." Some integrations also need to ask "what is the current state?" For that, Safeguard's API Reference names the hosted MCP server at https://mcp.safeguard.sh/mcp as the supported programmatic surface. Clients connect by URL and sign in with OAuth, or send an MCP key created under Settings, Developer as a bearer token. Its read-only tools list products, projects, vulnerabilities and findings, and fetch a single project or CVE record, with scope applied from the account you connect with.
A pattern that combines both: the webhook tells your ticket bridge that findings.vulnerability.severity_changed fired, and an agent with read-only MCP access fetches the current vulnerability record to write a better ticket description. Safeguard publishes no language SDK, and its SDKs page says to treat any package that claims to be a Safeguard SDK as untrusted. For receivers that is no loss, because signature verification needs only the standard library code shown above.
If you also run AI coding agents, the companion post on sending Claude Code and Codex telemetry to Safeguard with OpenTelemetry covers the opposite direction: data flowing into Safeguard.
Step by step: build a production receiver
This is a staged plan we suggest, assuming an HTTP receiver behind your normal ingress, a durable queue and a small worker.
- Decide the consumers. List each destination (SIEM, ticketing, on-call, data lake) and the event types it needs, using the routing map above. One subscription per consumer.
- Stand up the receiver in staging. It does four things only: enforce HTTPS and a body size limit, verify the signature against the raw body, write the raw body and key headers to a durable queue keyed on the event ID, and return 2xx. Bad signatures get 401, with nothing secret logged.
- Unit test the verifier with the self-signed
curlrequest and every negative case above. - Create the staging subscription on the Webhooks page: endpoint URL, event types, a new signing secret from your secret manager, and a filter if this consumer needs one.
- Send Safeguard test events. Confirm they verify, reach the queue and are dropped by the worker because of
"test": true, and that the Webhooks page shows first-attempt success. - Build the worker. It deduplicates on the event ID, applies the per-subject ordering check, routes on
typeand calls the destination. Destination failures retry inside your worker, never as failed deliveries to Safeguard. - Make ticket sync idempotent at the destination too. Store the Safeguard finding identifier in the ticket's external ID or a custom field, search before creating, close on
resolved, comment onsuppressed, reopen onpolicy.exception_expired. This keeps ticket counts honest after a DLQ replay. - Ship the SIEM feed. Send
audit.action,policy.*,attestation.verification_failed,scan.failedandworkflow.run.failedwith the raw payload, the API version and a "signature verified" flag. SP 800-92's advice on log integrity applies: the SIEM copy is the one nobody should be able to quietly edit. - Monitor the plumbing. Alert on a non-empty DLQ, a rising share of deliveries with attempt greater than 1, spikes in signature failures (a rotated secret or someone probing the endpoint) and unknown
typeorapi_versionvalues. - Go to production and widen. Create the production subscription, watch the Webhooks page for a week, then add consumers one at a time.
Securing the receiver itself
A webhook receiver is an internet-facing service that can create tickets, page people and write to your SIEM, so treat it like any other production service. Serve it over HTTPS only, because payloads describe your vulnerabilities. Load the signing secret from a secret manager and keep it out of logs and crash reports. Cap body and header size before verification, and do not parse, look up or log anything from the payload until the signature passes. Give the worker's ticketing token rights to create and update issues in the right projects and nothing more. And keep the receiver's own dependencies current: its web framework, JSON parser, HTTP client and base image are open source components too, and a vulnerable one is a door into every system the receiver writes to.
Common mistakes
- Verifying a re-serialised body. The framework parsed the JSON, the code re-stringified it, and every signature fails. Verify the raw bytes.
- Comparing signatures with
==, or lettingtimingSafeEqualthrow on a short signature. Use a constant-time compare with a length check and return 401. - Skipping the timestamp check. Without it, a captured delivery can be replayed forever.
- Processing inline. Calling the ticketing API before responding turns a slow dependency into retries and duplicates. Queue, then acknowledge.
- No deduplication. At-least-once delivery means duplicate tickets and duplicate pages unless you dedupe on the event ID.
- Assuming global order. Only per-subject, per-type order is guaranteed. We suggest that a late
severity_changedshould not reopen aresolvedfinding. - Listening only to
created. Tickets never close and suppressions leave no trail. - One secret for every consumer, so a leak in one bridge lets an attacker forge events to all of them.
- Reusing platform verification code for Guard. Different headers, retries and idempotency key.
- Never looking at the DLQ after an outage.
- Installing a package that claims to be a Safeguard SDK. Safeguard publishes none.
FAQ
How do I verify a Safeguard webhook signature?
Read the raw request body, split X-Safeguard-Signature into its v1, t and s parts, reject the request if t is more than 300 seconds from your clock, compute HMAC-SHA256 over t, a literal dot and the raw body using your subscription's signing secret, hex encode it, and compare it to s in constant time.
Does Safeguard retry failed webhook deliveries?
Yes. Platform webhooks use exponential backoff over 24 hours, with steps from 1 second up to 24 hours listed in the docs, and undelivered events go to a dead-letter queue you can replay from the UI. Guard webhooks retry only three times, after 1, 5 and 30 seconds, and then give up.
Can I receive the same Safeguard event twice?
Yes. Delivery is at least once. Deduplicate on X-Safeguard-Event-Id, which is stable across retries of the same event.
Are Safeguard webhook events delivered in order?
Only within one event type for a given subject, where created_at increases monotonically. Across types there is no ordering guarantee, so compare created_at per subject and type and, as our suggestion, let terminal states such as resolved win.
Which Safeguard events should go to a SIEM?
A sensible starting set is audit.action, all policy.* events, attestation.verification_failed, scan.failed and workflow.run.failed, plus the vulnerability lifecycle events if your SOC correlates them with other data. Filter the SIEM subscription lightly so the record stays complete.
How do I send Safeguard findings to Jira or ServiceNow?
Two ways. A workflow can create Jira or Linear issues as a step, with conditions on severity and reachability. For other ticketing systems, or for full lifecycle sync, subscribe a webhook receiver to the findings.vulnerability.* events and create, update and close tickets from a worker, keyed on the finding identifier.
Can I filter which events a webhook receives?
Yes. Each subscription can carry a server-side filter written in a subset of Safeguard's policy expression language, for example severity, environment and KEV status.
Is there a Safeguard SDK or CLI command for webhooks?
No. Subscriptions are created, edited and deleted on the Webhooks page, and Safeguard publishes no language SDK. Signature verification needs only your language's standard HMAC functions.
How do I test a Safeguard webhook endpoint?
Use Safeguard's synthetic test events, which carry "test": true and do not change real state. Before your endpoint is reachable, sign a sample payload yourself with openssl and post it with curl to test the verifier, including the failure cases.
Fixing what the events tell you
Events tell you quickly that something is wrong; they do not fix it. Most of what arrives on findings.vulnerability.created is an open source dependency or a container image with a known flaw, and the receiver you just built, with its web framework, JSON parser and base image, is made of the same kind of components.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Griffin, Safeguard's security model, opens a policy gated pull request that moves the affected dependency to the safe version, and your own CI runs your tests on it before anyone merges. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Auto-merge is a separate switch and stays off by default, so your team decides what merges on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Safeguard Gold rebuilds patched versions of open-source packages and images from source, signs each build, attaches an SBOM, SLSA provenance and VEX, and serves them from registry.safeguard.sh to your organization. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
Webhooks push events out; sometimes a person or an agent needs to ask a follow-up question about the finding behind an event. This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude may use, the same read-only path a ticket bridge can use to enrich what a webhook delivered.
Click through it at your own pace, or open it full screen.
The second demo is a real Safeguard CLI session. A developer asks the assistant to fix a lodash CVE; it reads the finding and advisory, asks before any command that changes something, updates the dependency, runs the install and tests with approval, and pushes nothing. It is what a developer does after a findings.vulnerability.created ticket lands in their queue.
Click through it at your own pace, or open it full screen.
The third demo sets up the local runner on your own machine and scans from the terminal. Jobs run where the code lives and only the SBOM and findings go back, and those findings then flow out through the same event stream to your systems.
Click through it at your own pace, or open it full screen.
To try Safeguard yourself, download the apps or read the Webhooks and Events documentation. For every other system Safeguard connects to, see every Safeguard integration explained.
Quick check
Your receiver parses JSON, then verifies the signature over JSON.stringify of the parsed body. Most deliveries fail. Why?
The signature covers the raw bytes Safeguard sent. Re-serialising can change whitespace and member order, so the bytes differ. Verify against the raw body before parsing.
The same findings.vulnerability.created event arrives twice, with X-Safeguard-Delivery-Attempt 1 and then 2. Is something broken?
Not necessarily. Delivery is at least once, so a slow or failed first response leads to a retry with the same event ID. Deduplicate on X-Safeguard-Event-Id, and look at why the first attempt failed or was slow.
A Guard alert receiver was down for two minutes. Will the alerts be redelivered later from a dead-letter queue?
No. Guard webhooks retry after 1, 5 and 30 seconds and then give up. Treat Guard's Audit Trail and Alerts as the durable record and use the webhook for low-latency paging.
Sources
- Safeguard docs: Webhooks and Events
- Safeguard docs: Guard Webhooks
- Safeguard docs: Slack, Teams and Discord
- Safeguard docs: Workflows
- Safeguard docs: API Reference
- Safeguard docs: SDKs
- Safeguard docs: Integrations
- Safeguard docs: Guardrails and Enforcement
- Safeguard docs: Local Runner
- IETF RFC 2104: HMAC, Keyed-Hashing for Message Authentication (February 1997)
- NIST FIPS 198-1: The Keyed-Hash Message Authentication Code (HMAC) (July 2008)
- IETF RFC 9110: HTTP Semantics, section 10.2.3 Retry-After
- IETF RFC 6585: Additional HTTP Status Codes (429 Too Many Requests)
- IETF RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format
- IETF RFC 3339: Date and Time on the Internet: Timestamps
- Python documentation: hmac, Keyed-Hashing for Message Authentication
- Node.js documentation: crypto.timingSafeEqual
- CloudEvents specification (CNCF)
- Amazon Kinesis Data Streams: Handle duplicate records
- Google Cloud Pub/Sub: Exactly-once delivery
- Microsoft Learn: Azure Event Hubs overview
- NIST SP 800-92: Guide to Computer Security Log Management (September 2006)
- NIST SP 800-61 Rev. 3: Incident Response Recommendations and Considerations for Cybersecurity Risk Management (April 2025)
- CISA: Known Exploited Vulnerabilities Catalog
Last reviewed: 27 October 2026.