XDR Pipeline

Bidirectional connectors between BrainstormRouter and your SIEM. OCSF 1.3.0 events flow out, risk scores flow back in to feed the trust envelope.

Overview

BrainstormRouter speaks OCSF 1.3.0 (Open Cybersecurity Schema Framework) to your SIEM / XDR platform. Events that BR's internal event bus produces — auth failures, cert lifecycle, routing rejections, PII detections, anomaly spikes, audit-chain events — are projected into OCSF and delivered to one or more configured destinations. In the reverse direction, your XDR pushes risk scores back into BR via POST /v1/admin/xdr/risk-push, and the next trust envelope synthesized for that agent picks up the score as br_trust.xdr_risk.

The pipeline is per-tenant — every tenant configures its own destinations independently. Events are tenant-scoped (the OCSF unmapped.tenant_id field) so a multi-tenant SIEM can fan out alerts by tenant.

Supported destinations

Five first-party SIEM / XDR connectors plus the generic webhook adapter:

Destination typeAuthSource files
crowdstrikeOAuth2 client_credentials → Threat Graph APIsrc/observability/xdr/adapters/crowdstrike.ts
sentinelAzure AD OAuth2 → Log Analytics (DCR or DCE)src/observability/xdr/adapters/sentinel.ts
splunk-ocsfHEC token → Splunk HECsrc/observability/xdr/adapters/splunk-ocsf.ts
cortexPer-request SHA256 hash (SHA256(api_key + nonce + timestamp))src/observability/xdr/adapters/cortex.ts
datadog-ocsfAPI key → Datadog Logs Intakesrc/observability/xdr/adapters/datadog-ocsf.ts
webhookOptional HMACGeneric — see Observability

Cortex's per-request hashing scheme is unusual: each request carries a fresh nonce and the auth header is SHA256(api_key + nonce + timestamp) — a plain SHA256 hash over the concatenation, not an HMAC. The adapter generates the nonce per-event so a leaked log line cannot be replayed.

Event taxonomy

The internal event bus emits 52 event types across nine categories (after X-2). The OCSF projector (src/observability/xdr/ocsf-projector.ts) maps each to the appropriate OCSF class (6003 API Activity for telemetry events, 2006 Data Security Finding for security events) with a stable type_uid per event variant.

Categories

CategoryExamplesOCSF class
Tenant lifecycletenant.signup, tenant.approved, tenant.claimedAPI Activity
Provider healthprovider.down, provider.recovered, provider.auth_errorAPI Activity
Model catalogmodel.discovered, model.deprecatedMixed
Intelligenceintelligence.circuit.opened, intelligence.budget.exceeded, intelligence.anomaly.detectedMixed
Identity / Authauth.success, auth.failure, cert.issued, cert.revoked, agent.bootstrappedAPI Activity
Authorizationauthz.denied, authz.scope_exceeded, authz.tool_blockedFinding
Routingrouting.cascade_triggered, routing.fallback_activated, routing.rejectedMixed
Behavioralbehavior.anomaly_spike, behavior.rate_limit_hit, behavior.pattern_detectedFinding
Data flowdata.pii_detected, data.guardrail_triggered, data.owasp_violationFinding
Audit chainaudit.envelope_signed, audit.envelope_verified, audit.chain_brokenMixed

Some events are deliberately mapped to null in the EVENT_MAP — they are high-volume telemetry (routing.model_selected, audit.envelope_signed, auth.token_refresh) that exists for downstream consumers but should not fan out to a SIEM by default. Tenants who want the firehose can flip the mapping per-destination via filter rules.

A completeness-gate test (event-map-coverage.test.ts) fails CI if a new PlatformEventType is added without an EVENT_MAP decision (delivery target or explicit null).

PII hard rule

OCSF events NEVER include raw prompt or response text. SIEMs are the wrong place for content. The projector outputs only:

  • Event metadata (type, timestamp, severity, uid)
  • Principal IDs (tenantId, agentId, userId — never email unless hashed)
  • Model / provider names (not model outputs)
  • Validity / anomaly scores (numeric, not the underlying content)
  • Content hashes when downstream correlation is needed

A dedicated test (no-pii-leak.test.ts) fuzzes every PlatformEvent variant with email and credential sentinels and asserts the strings never appear in the OCSF output. Any future change that adds prompt, response, content, or message fields to the projector output will fail this test.

If a downstream consumer needs the actual content, it must call BR's audit API directly with proper auth — never piggyback on the SIEM stream.

Filter rules (X-4)

Per-destination filter_rules constrain what events a destination receives:

{
  "filter_rules": {
    "include": ["intelligence.*", "auth.*", "data.*"],
    "exclude": ["intelligence.cache.*"],
    "min_severity": 3
  }
}
  • include — list of glob patterns. Empty = match all.
  • exclude — list of glob patterns. Applied after include.
  • min_severity — OCSF severity ID minimum (1=Informational, 2=Low,

3=Medium, 4=High, 5=Critical). Applied after type filters.

Glob matching uses as a [^.]+ placeholder: intelligence. matches intelligence.budget.exceeded because . is a literal separator, not a wildcard match-anything.

Bidirectional risk push (X-7)

POST /v1/admin/xdr/risk-push accepts a risk score from your XDR. Both tenant_id and agent_id must be UUIDs; source is required (logged on every push for audit attribution); ttl_seconds defaults to 86400 (24h) and is capped at 2,592,000 (30 days).

Authorization: Bearer <admin-key>
Content-Type: application/json

{
  "tenant_id": "9f3b1e0c-c4a1-4a8e-9c8b-2e3f4a5b6c7d",
  "agent_id":  "1a2b3c4d-5e6f-7890-abcd-ef0123456789",
  "risk_score": 0.83,
  "source": "crowdstrike-falcon",
  "ttl_seconds": 86400
}

Auth: Platform admin only — RBAC requires the platform.admin permission (same gate as all other /v1/admin/* routes).

Feature flag: This endpoint returns 403 feature_disabled unless the XDR_BIDIRECTIONAL_ENABLED=1 environment variable is set on the gateway task. The default is OFF — flip it on per-environment when the partner XDR is ready to push.

Dry run: Append ?dry_run=true to verify connectivity without persisting. The response echoes { dry_run: true, would_set: { ... } }.

Behavior on a real push:

  1. The score is stored at xdr:risk:{tenantId}:{agentId} in Redis with

the supplied TTL.

  1. The next envelope synthesized for that agent reads the score into

br_trust.xdr_risk.

  1. If xdr_risk >= 0.7, the routing gate force-downgrades to

restricted-equivalent regardless of nominal tier.

The score auto-expires — XDR-driven downgrade is temporary by construction. To extend, push a new score; to clear early, call DELETE /v1/admin/xdr/risk-push with the same tenant_id + agent_id in the request body:

DELETE /v1/admin/xdr/risk-push?dry_run=false
Authorization: Bearer <admin-key>
Content-Type: application/json

{ "tenant_id": "9f3b...", "agent_id": "1a2b..." }

The clear endpoint shares the same auth gate, feature flag, and dry_run query parameter as the push endpoint.

Per-tenant config CRUD (X-8)

Tenants manage their own destinations through seven endpoints under /v1/observability/xdr/destinations:

MethodPathPurpose
GET/v1/observability/xdr/destinationsList the tenant's destinations
POST/v1/observability/xdr/destinationsCreate a destination
PUT/v1/observability/xdr/destinations/{id}Update
DELETE/v1/observability/xdr/destinations/{id}Delete
POST/v1/observability/xdr/destinations/{id}/testSend 1 test event to verify connectivity
GET/v1/observability/xdr/destinations/{id}/statsDelivery stats (last 24h)
POST/v1/observability/xdr/destinations/{id}/enableEnable / disable without deleting

Secret fields (apiKey, clientSecret, hecToken, workspaceKey, secretAccessKey, snake-case variants) are redacted in all responses. Writing a new secret replaces the prior one; reading never returns it.

The xdr_destinations Drizzle table (DB schema v52) is RLS-scoped on tenant_id so cross-tenant reads are impossible at the SQL layer.

Latency budget

Stagep50p99
Event emission (synchronous)<1 ms5 ms
OCSF projection0.2 ms1 ms
Filter-rule evaluation0.1 ms0.5 ms
Adapter delivery (per HTTP)50 ms300 ms

Adapter delivery runs in the background subscriber — it does not block the request path. A failed delivery is retried with exponential backoff up to 3 times; after that, the event is dropped and a xdr.delivery.failed metric is incremented.

Related concepts