Send GaaS alerts to your SIEM

Point your organization's own webhooks at Splunk, Microsoft Sentinel, Datadog, or any SIEM that accepts HTTPS. Every delivery is signed, retried, and recorded if it fails.

How it works

When GaaS blocks or escalates an agent's action, it can POST the event to an HTTPS address that your organization registers. That is the whole integration: there is no separate SIEM feed to switch on. Each organization has its own webhooks, its own signing secrets and its own delivery records, so your events never share a pipe with anyone else's.

Some SIEMs accept GaaS's POST exactly as it is sent. Others want their own envelope, or an auth header GaaS doesn't send. For those, a small adapter sits in between: it checks the signature, reshapes the event and forwards it.

SIEMStraight from GaaS?Why
DatadogYesThe logs intake takes any JSON object and accepts the API key as a URL parameter.
Splunk (HTTP Event Collector)No, use the adapterHEC wants an Authorization: Splunk <token> header and an {"event": …} envelope.
Microsoft SentinelNo, use a Logic App or the adapterThe Logs Ingestion API needs a Microsoft Entra token and a JSON array.
Anything elseUsually via the adapterVerify, reshape, forward.
Reading from your SIEM instead? GaaS can also query Splunk, QRadar or Sentinel for a user's risk while it evaluates an intent. That is the inbound SIEM connector on the Connectors page. This page is about sending GaaS's decisions out.

Which events to send

EventSent whenWorth sending to a SIEM?
decision.blockedGaaS blocked an agent's actionYes, the main signal
decision.escalatedGaaS held an action for human reviewYes
escalation.decidedA reviewer approved or rejected a held actionUsually, it closes the loop on an escalation
escalation.timed_outNobody reviewed a held action in timeYes
escalation.cancelled, escalation.reassignedAn escalation was cancelled, or handed to other reviewersOptional
quota.exceededYour organization reached its hard usage limit, so GaaS is refusing new intentsYes
decision.overriddenA person approved a blocked action, so the agent's next retry of that same action is approved onceYes: a person set a block aside
decision.approvedAn action was approved, with or without modificationsUsually not: it fires for every approved action

Decision events are sent in every pipeline mode. The data.pipeline_mode field tells you which one: live, shadow or test. Most SIEM rules should act on live only.

Not delivered today: registration also accepts rate_limit.exceeded, observation.recorded, policy.calibrated and pattern.detected, but the hosted service does not currently send them. Don't build alerts on them.

Register a webhook

Use an API key with the admin role. List only the events you want; if you leave out event_types, the webhook receives every event type.

curl -X POST https://api.gaas.is/v1/escalations/webhooks \
  -H "X-API-Key: $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://siem-adapter.example.com/gaas",
    "event_types": ["decision.blocked", "decision.escalated", "escalation.timed_out", "quota.exceeded"],
    "description": "SIEM forwarder"
  }'

GaaS answers 201 Created:

{
  "webhook": {
    "id": "wh_81af440d0481",
    "created_at": "2026-09-28T17:52:54.924800Z",
    "url": "https://siem-adapter.example.com/gaas",
    "organization_id": "org_acme",
    "escalation_id": null,
    "event_types": [
      "decision.blocked",
      "decision.escalated",
      "escalation.timed_out",
      "quota.exceeded"
    ],
    "secret": "whsec_5b0e6c1d9a8f4e2b8c7d6e5f4a3b2c1d",
    "active": true,
    "description": "SIEM forwarder"
  }
}

To see what is registered, call GET /v1/escalations/webhooks. To stop deliveries, call DELETE /v1/escalations/webhooks/{webhook_id} with an admin key.


What GaaS sends

Each delivery is an HTTPS POST with these headers:

HeaderValue
Content-Typeapplication/json
X-GaaS-Signaturesha256= followed by the hex HMAC-SHA256 of the body (see Verify the signature)
X-GaaS-EventThe event type, for example decision.blocked
X-GaaS-DeliveryThe delivery ID (dlv_…). Automatic retries of one delivery reuse it.
X-GaaS-Retrytrue, only on a manual retry

A decision.blocked body, indented here for reading:

{
  "data": {
    "decision_id": "dec_0f0de7b67fcd4e1487fe6bc5030eacf6",
    "human_override_id": null,
    "intent_id": "int_85e46e12d8d949ff9c207bffac3afca5",
    "pipeline_mode": "live",
    "risk_score": 0.562,
    "timestamp": "2026-09-28T17:52:54.955690+00:00",
    "verdict": "block"
  },
  "decision_id": "dec_0f0de7b67fcd4e1487fe6bc5030eacf6",
  "escalation_id": null,
  "escalation_status": null,
  "event_type": "decision.blocked",
  "intent_id": "int_85e46e12d8d949ff9c207bffac3afca5",
  "observation_id": null,
  "organization_id": "org_acme",
  "pattern_id": null,
  "policy_id": null,
  "timestamp": "2026-09-28T17:52:54.959796Z",
  "webhook_id": "wh_81af440d0481"
}

On the wire the body is compact JSON with its keys in alphabetical order. Always check the signature against the bytes you received, never against JSON you have parsed and re-encoded.

FieldMeaning
event_typeSame as the X-GaaS-Event header
timestampWhen GaaS built this delivery (UTC)
webhook_id, organization_idWhich of your webhooks, and which organization
intent_id, decision_idSet on decision events; null otherwise
escalation_id, escalation_statusSet on escalation events; null otherwise
data Decision events: verdict (approve, approve_modified, escalate or block, always lowercase), risk_score (0 to 1), pipeline_mode, the decision's own timestamp, and human_override_id — set when a person's approval turned a block into this approval, otherwise null.
decision.overridden: intent_id and decision_id of the blocked action, override_id, agent_id, approved_by, approved_at, expires_at and policies_overridden.
Escalation events: the full escalation record.
quota.exceeded: plan_id, included_actions, used_actions, hard_limit, batch_size, timestamp.
observation_id, policy_id, pattern_idAlways null in the events listed above
Need the agent, the action, or the policies that fired? Decision events don't carry them. Fetch the full, hash-chained audit record with GET /v1/intents/{intent_id}/audit, using any API key from your organization.

Verify the signature

The signature in X-GaaS-Signature is built like this:

import hashlib
import hmac


def is_from_gaas(headers: dict[str, str], raw_body: bytes, secret: str) -> bool:
    """True if X-GaaS-Signature matches the exact bytes that were received."""
    received = next((v for k, v in headers.items() if k.lower() == "x-gaas-signature"), "")
    expected = "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(received, expected)

The signature covers the body only; it carries no timestamp. To stop someone replaying a captured request, remember the X-GaaS-Delivery IDs you have already accepted and ignore repeats. Automatic retries reuse the same ID and the same bytes, so this also removes duplicates.


Retries and failed deliveries

List failed deliveries

Use an operator or admin key. Add ?webhook_id=wh_… to narrow the list to one webhook.

curl https://api.gaas.is/v1/escalations/webhooks/deliveries/failed \
  -H "X-API-Key: $OPERATOR_API_KEY"
{
  "deliveries": [
    {
      "id": "dlv_d4cadf8e3fc6",
      "webhook_id": "wh_81af440d0481",
      "event_type": "decision.blocked",
      "status": "failed",
      "attempts": 3,
      "max_attempts": 3,
      "last_attempt_at": "2026-09-28T17:55:31.755677Z",
      "response_status": 503,
      "error": null,
      "created_at": "2026-09-28T17:55:31.737650Z",
      "escalation_id": null,
      "intent_id": "int_bc99eb55235c40d997a6cef7df9dc321",
      "entity_id": "int_bc99eb55235c40d997a6cef7df9dc321"
    }
  ],
  "total": 1
}

Retry one

curl -X POST https://api.gaas.is/v1/escalations/webhooks/deliveries/dlv_d4cadf8e3fc6/retry \
  -H "X-API-Key: $OPERATOR_API_KEY"

The answer is {"success": true|false, "delivery_id": "…", "message": "…"}. A delivery that doesn't exist, or hasn't failed, gets 404.

A manual retry does not resend the original event. It sends a short notice with X-GaaS-Retry: true, the same event type, and data.original_delivery_id. Its intent_id and decision_id are null. To recover what was missed, take intent_id from the failed-delivery record and fetch GET /v1/intents/{intent_id}/audit. For a whole time window, use GET /v1/audit/export/stream?start_date=…&end_date=… (operator or admin key), which returns one audit record per line.

Webhooks are a live feed, not the system of record. The audit trail is. If your SIEM has to be complete, reconcile it against the audit export on a schedule.


Datadog: send directly

Register this as the webhook URL. It is shown for the US1 site; use the intake host for your own Datadog site.

https://http-intake.logs.datadoghq.com/api/v2/logs?dd-api-key=YOUR_DATADOG_API_KEY&ddsource=gaas

Splunk: HTTP Event Collector, via the adapter

HEC's /services/collector/event endpoint expects an Authorization: Splunk <token> header and the event inside an "event" key. GaaS sends neither. Its body has no "event" key, so HEC refuses it with status code 12, "Event field is required" (HTTP 400).

HEC can take the token in the query string instead, but only if query-string authentication is enabled for that token (allowQueryStringAuth = true; on Splunk Cloud Platform, through a support case). The envelope is still required, so the adapter is the practical route. Give it this wrap function:

from datetime import datetime


def to_splunk(event: dict) -> dict:
    return {
        "time": datetime.fromisoformat(event["timestamp"]).timestamp(),  # UNIX time (Python 3.11+ parses the Z)
        "source": "gaas",
        "sourcetype": "gaas:webhook",
        "event": event,
    }

# SIEM_URL     = "https://YOUR-HEC-HOST:8088/services/collector/event"
# SIEM_HEADERS = {"Authorization": "Splunk YOUR_HEC_TOKEN"}

Microsoft Sentinel: a Logic App, or the adapter

Sentinel reads from a Log Analytics workspace. The supported way to push custom events into one is the Azure Monitor Logs Ingestion API, and that API needs a Microsoft Entra bearer token, a data collection rule (DCR) and a JSON array body. A GaaS webhook can't supply any of those, so something sits in between.

Both options need:

Option A: Logic App (no code)

  1. Start a workflow with the When a HTTP request is received trigger and save it. Copy the URL it generates. The URL contains a shared access signature (sig=…), so treat it as a secret. Register it as your GaaS webhook URL.
  2. Leave out a Response action. The trigger then answers 202 straight away, which GaaS counts as delivered.
  3. Add an HTTP action that POSTs to {endpoint}/dataCollectionRules/{dcrImmutableId}/streams/{streamName}?api-version=2023-01-01, with authentication set to the Logic App's managed identity and Audience set to https://monitor.azure.com. Send the trigger body, wrapped in a JSON array, as application/json.

This option does not check X-GaaS-Signature. The secret sig in the trigger URL is what keeps others out, and anyone in your GaaS organization who lists webhooks can see that URL.

Option B: the adapter, with the signature checked

Run the adapter (an Azure Function works) and, instead of forwarding over plain HTTP, upload with the Azure Monitor Ingestion client library (pip install azure-monitor-ingestion azure-identity):

from azure.identity import DefaultAzureCredential
from azure.monitor.ingestion import LogsIngestionClient

client = LogsIngestionClient(endpoint=YOUR_DCR_OR_DCE_ENDPOINT, credential=DefaultAzureCredential())
client.upload(rule_id=YOUR_DCR_IMMUTABLE_ID, stream_name=YOUR_STREAM_NAME, logs=[event])

A tiny adapter for any SIEM

This function checks one GaaS delivery and forwards it. Call it from any web framework or serverless function: pass the request headers and the exact raw body bytes, and answer GaaS with the status it returns. It needs only the Python standard library.

import hashlib
import hmac
import json
import os
import urllib.request

SECRET = os.environ["WEBHOOK_SECRET"]                           # the whsec_… value from registration
SIEM_URL = os.environ["SIEM_URL"]                               # where your SIEM accepts events
SIEM_HEADERS = json.loads(os.environ.get("SIEM_HEADERS", "{}"))  # e.g. {"Authorization": "Splunk …"}


def handle(headers: dict[str, str], raw_body: bytes, wrap=lambda event: event) -> int:
    """Verify one GaaS delivery and forward it. Returns the status to answer GaaS with."""
    h = {k.lower(): v for k, v in headers.items()}
    expected = "sha256=" + hmac.new(SECRET.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(h.get("x-gaas-signature", ""), expected):
        return 401                                              # not from GaaS: refuse it
    if h.get("x-gaas-retry") == "true":
        return 200                                              # manual-retry notice: no event data inside
    body = json.dumps(wrap(json.loads(raw_body))).encode("utf-8")
    request = urllib.request.Request(SIEM_URL, data=body, method="POST",
                                     headers={"Content-Type": "application/json", **SIEM_HEADERS})
    try:
        with urllib.request.urlopen(request, timeout=5) as response:
            return 200 if 200 <= response.status < 300 else 502
    except OSError:
        return 502                                              # SIEM unreachable or refused: GaaS retries

Vendor sources

The vendor details on this page were checked against these pages on 2026-09-28:


Related Pages