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 type | Auth | Source files |
|---|---|---|
crowdstrike | OAuth2 client_credentials → Threat Graph API | src/observability/xdr/adapters/crowdstrike.ts |
sentinel | Azure AD OAuth2 → Log Analytics (DCR or DCE) | src/observability/xdr/adapters/sentinel.ts |
splunk-ocsf | HEC token → Splunk HEC | src/observability/xdr/adapters/splunk-ocsf.ts |
cortex | Per-request SHA256 hash (SHA256(api_key + nonce + timestamp)) | src/observability/xdr/adapters/cortex.ts |
datadog-ocsf | API key → Datadog Logs Intake | src/observability/xdr/adapters/datadog-ocsf.ts |
webhook | Optional HMAC | Generic — 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
| Category | Examples | OCSF class |
|---|---|---|
| Tenant lifecycle | tenant.signup, tenant.approved, tenant.claimed | API Activity |
| Provider health | provider.down, provider.recovered, provider.auth_error | API Activity |
| Model catalog | model.discovered, model.deprecated | Mixed |
| Intelligence | intelligence.circuit.opened, intelligence.budget.exceeded, intelligence.anomaly.detected | Mixed |
| Identity / Auth | auth.success, auth.failure, cert.issued, cert.revoked, agent.bootstrapped | API Activity |
| Authorization | authz.denied, authz.scope_exceeded, authz.tool_blocked | Finding |
| Routing | routing.cascade_triggered, routing.fallback_activated, routing.rejected | Mixed |
| Behavioral | behavior.anomaly_spike, behavior.rate_limit_hit, behavior.pattern_detected | Finding |
| Data flow | data.pii_detected, data.guardrail_triggered, data.owasp_violation | Finding |
| Audit chain | audit.envelope_signed, audit.envelope_verified, audit.chain_broken | Mixed |
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 afterinclude.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:
- The score is stored at
xdr:risk:{tenantId}:{agentId}in Redis with
the supplied TTL.
- The next envelope synthesized for that agent reads the score into
br_trust.xdr_risk.
- 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:
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/observability/xdr/destinations | List the tenant's destinations |
| POST | /v1/observability/xdr/destinations | Create a destination |
| PUT | /v1/observability/xdr/destinations/{id} | Update |
| DELETE | /v1/observability/xdr/destinations/{id} | Delete |
| POST | /v1/observability/xdr/destinations/{id}/test | Send 1 test event to verify connectivity |
| GET | /v1/observability/xdr/destinations/{id}/stats | Delivery stats (last 24h) |
| POST | /v1/observability/xdr/destinations/{id}/enable | Enable / 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
| Stage | p50 | p99 |
|---|---|---|
| Event emission (synchronous) | <1 ms | 5 ms |
| OCSF projection | 0.2 ms | 1 ms |
| Filter-rule evaluation | 0.1 ms | 0.5 ms |
| Adapter delivery (per HTTP) | 50 ms | 300 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
- Trust Envelope —
br_trust.xdr_riskis what XDR feeds - Agent Reputation — XDR risk ≥ 0.7 forces tier downgrade
- Observability — generic event bus + webhooks (X-4 filter rules apply equally)