Gatana logoGatana Docs

SIEM Streaming

Stream Gatana audit events to your own SIEM as HMAC-signed NDJSON batches over HTTPS.

Introduction

SIEM streaming delivers your organization's audit events to a security platform you control. Gatana pushes events to an HTTPS endpoint you configure, so your own detection rules, dashboards, and retention apply to Gatana activity alongside the rest of your estate.

It works with any collector that accepts an authenticated HTTPS POST, including Splunk HTTP Event Collector, CrowdStrike Falcon LogScale, Elastic, Microsoft Sentinel through a relay, and in-house pipelines.

Configure it under Settings → SIEM Streaming in the dashboard. One destination per organization. The feature requires a paid plan.

Event sources

Two sources are streamed, and each can be turned off independently.

SourceContents
audit_logsServer, profile, team, and organization configuration changes; sign-ins; user and team membership changes; secret fetches; and every MCP request, including tool calls
audit_logs_credentialsCredential creation, updates, and deletion, plus automatic and manual OAuth token refreshes

How much detail MCP events carry is controlled by the MCP audit log level in your organization settings; see Audit Logs for the levels. At terse, the default, an event records which tool was called, how long it took, and which credential made the call.

Internal events visible only to Gatana staff, such as billing webhook traffic, are never streamed.

Envelope schema

Each event is one JSON object on its own line.

{
  "id": "audit_logs:184223",
  "version": 1,
  "time": "2026-08-04T09:52:04.118Z",
  "tenant": "acme",
  "source": "audit_logs",
  "action": "mcp_server.update",
  "actor": { "type": "user", "id": "usr_8Fq2..." },
  "entity": { "type": "mcp_server", "id": "srvr_pQ1..." },
  "details": {}
}
FieldDescription
idStable, unique, and repeatable for the same event. Use it to deduplicate
versionEnvelope version. A new version is introduced only for a breaking change
timeWhen the event happened, ISO 8601 UTC
tenantYour organization identifier
sourceaudit_logs or audit_logs_credentials
actionDotted action name, described below
actor{ "type": "user", "id": "..." }, or { "type": "system", "id": null } for background work and database triggers
entityWhat the event is about. Either field can be null
detailsEvent-specific payload. Shape varies by action

Event catalog

Most actions follow {entity type}.{event name}, so an action appears for a new kind of event without any change on your side. Write rules against the prefix where you can.

ActionMeaning
mcp_server.create / .update / .deleteAn MCP server was added, changed, or removed
mcp_server.start / .stopA hosted server was started or stopped
mcp_server.credentials.upsert / .delete / .delete_all / .copyServer credentials were changed
profile.create / .update / .deleteA profile was changed
team.create / .update / .deleteA team was changed
team.member_join / .member_leaveTeam membership changed
tenant_configuration.updateOrganization settings changed
siem_destination.create / .update / .deleteThis streaming configuration itself changed
connected_client.create / .update / .deleteA user's OAuth client connection appeared, was configured, or was disconnected
connected_client_profile.create / .deleteA profile was attached to or detached from a connected client
user.login_successA user signed in
user.created / .disabled / .disabled_due_to_seat_limitA user account changed
secret_store.secret_fetchedA secret was read from an external secret store
firewall.deny / firewall.logEgress from a hosted server was blocked or recorded
mcp.tools/callA tool was invoked. Other JSON-RPC methods appear under the same mcp. prefix
credential.create / .update / .deleteA credential record changed
credential.auto_oauth_refresh_success / .auto_oauth_refresh_failedGatana refreshed an OAuth token in the background
credential.manual_oauth_refresh_success / .manual_oauth_refresh_failedA user refreshed an OAuth token
credential.expired_no_refreshAn access token expired with no refresh token available
gatana.testA test event sent from the dashboard

Configuration changes are audited too

Changes to the streaming destination arrive in the stream as siem_destination.*, so a rule can alert when somebody redirects your audit trail. Delivery bookkeeping such as the internal read position is not an event and produces nothing.

Delivery

Batches are POSTed as application/x-ndjson, one event per line, up to 500 events or roughly 1 MB per request. The exporter runs once a minute and events are held back for a few seconds so that no event is skipped, so expect events within about a minute of happening.

Respond 2xx as soon as you have accepted the batch. Requests time out after 10 seconds, and redirects are not followed.

Delivery is at-least-once. A batch that is accepted but whose acknowledgement is lost is sent again, so the same event can arrive twice. Deduplicate on id. Events from each source arrive in order.

If your endpoint fails, Gatana retries with a widening delay of 1, 5, 15, and then 60 minutes:

  • after 1 hour of failures the destination is marked failing in the dashboard
  • after 24 hours your administrators receive one email
  • after 7 days delivery is disabled and a second email is sent

Nothing is lost while a destination is failing or paused. The read position is kept, so reactivating resumes where delivery stopped, as far back as audit retention allows.

Verifying signatures

Every request carries a signature over the exact body:

X-Gatana-Signature: t=1785825392,v1=6c3b...f19a

t is a Unix timestamp in seconds and v1 is HMAC-SHA256 of "{t}.{raw body}" keyed with your signing secret. The secret is shown once when you configure the destination and can be revealed again from the dashboard.

Verify against the raw request body, before any JSON parsing. Reject a request whose timestamp is outside a tolerance you choose; five minutes is a reasonable default.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!Number.isFinite(age) || age > toleranceSeconds) return false;

  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1 ?? '', 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(int(time.time()) - int(parts["t"])) > tolerance_seconds:
        return False
    expected = hmac.new(
        secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Rotating the secret takes effect immediately, so update your receiver first or accept a short gap. If your receiver can check two secrets at once, add the new one before rotating.

What is never sent

  • Secret values. Tokens, passwords, client secrets, private keys, and API keys are replaced with [REDACTED] wherever they appear, at any depth.
  • Encrypted secret columns. A change to a stored secret is reported as {"changed": true} and never decrypted. Encrypted configuration, such as a server's transport settings, is included so that you can see what changed, with any embedded credentials redacted inside it.
  • Anything Gatana cannot decrypt appears as <DECRYPT_FAILED> rather than blocking the stream.

A details payload larger than 64 KB is replaced with {"_truncated": true, "_originalBytes": …, "preview": "…"}. This mainly affects verbose MCP logging, where a whole tool response is recorded.

Receiver setup

The endpoint URL must use HTTPS. In Gatana Cloud it must also resolve to a public address: internal hostnames and private address ranges are refused, because a shared platform must not be pointed at infrastructure it should not reach. Self-hosted deployments have no such boundary, so they may stream to a collector on their own network, such as https://splunk.internal/… or a private address. HTTPS is required either way, so audit data never travels in the clear.

Use the optional auth header to carry whatever credential your collector expects, for example Authorization: Splunk <token> for Splunk HTTP Event Collector, or Authorization: ApiKey <key> for Elastic. The value is stored encrypted and never returned by the API.

Splunk HTTP Event Collector. Point the URL at https://<host>:8088/services/collector/raw and set the auth header to Authorization with the value Splunk <hec-token>. The raw endpoint accepts newline-delimited JSON directly. Set sourcetype on the token or its input.

CrowdStrike Falcon LogScale. Use the HEC-compatible ingest endpoint of your repository, https://<host>/api/v1/ingest/hec/raw, with Authorization: Bearer <ingest-token>.

Elastic. Send to an HTTP input on your ingest pipeline, then split lines and parse each as JSON.

Anything else. Any endpoint accepting an authenticated HTTPS POST of newline-delimited JSON works. Verify the signature, split on newlines, parse each line, and deduplicate on id.

Use Send test event under Settings to deliver a single gatana.test event. The dialog shows the HTTP status and the beginning of the response body, which is usually enough to identify a rejected credential or a wrong path.

On this page