MainSDK
Search⌘K
Interfaces

Security and privacy

Security events

security

Write security events in OCSF with ATT&CK tags, detect brute force, stuffing and probing in the app, and stream them to your own SIEM.

Building for 0.3ships as a beta APIPhase 1 · Core, public betaSpec security-eventsFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

Write security events in OCSF with ATT&CK tags, detect brute force, stuffing and probing in the app, and stream them to your own SIEM.

At a glance

security:
  events:
    to: [log, otel]
    min: low
    regions:
      eu: { endpoint: https://otel.eu.acme.dev:4318, headers: { authorization: secret:OTEL_EU_TOKEN } }
  detect:
    brute-force: { failures: 10, window: 5m }
    stuffing: { accounts: 20, window: 5m }
    probing: { denials: 20, window: 1m }

Calls

  • security.emit({ type, severity, message, attrs?, attack? })
  • security.on(type, fn)

Guarantees

Whatever the driver.

  • Every event is OCSF with ATT&CK tags and no personal data (keyed user hash, trimmed IP)
  • Never on the request path; export is buffered and drops events below high first
  • Each event goes to the endpoint of its tenant's region
  • Detectors run in the app with bounded counters

Drivers

1 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (log, OTLP and events sinks) (default)
security:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-SECURITY-0001Checkmainsdk check, before deployA security value is invalid: an unknown severity, sink or detector, or a detector threshold below 2
MAIN-SECURITY-0002Checkmainsdk check, before deployto includes otel and a region the app stores data in has no endpoint (no regions.<r> and no telemetry.otel)
MAIN-SECURITY-0003RuntimeError responses and logsSee the spec
MAIN-SECURITY-0004RuntimeError responses and logsSee the spec
MAIN-SECURITY-0005RuntimeError responses and logsSee the spec
MAIN-SECURITY-0010RuntimeError responses and logsSee the spec
MAIN-SECURITY-0011RuntimeError responses and logsSee the spec
MAIN-SECURITY-0012RuntimeError responses and logsSee the spec

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
Event typePhase 10.3security-events#contract
security.emit and security.onPhase 10.3security-events#emit
Producers: sign-in refusals, access denials and refused egress (others pending)Phase 10.3security-events#producers
Brute force, credential stuffing and access probing detectorsPhase 10.3security-events#detectors
Log, OTLP and events sinks by regionPhase 10.3security-events#sinks
Bounded export bufferPhase 10.3security-events#buffer
mainsdk logs --security and the security line in mainsdk infoPhase 10.3security-events#cli
Overhead budgetPhase 10.3security-events#budget
Sigma rules for the detectionsPhase 10.3security-events#sigma

MainSDK Cloud

Everything above is free in your app. Cloud adds these on top, by plan:

PlanWhat it adds
FleetCorrelation across apps and managed SIEM connectors (spec fleet-defend)

Quick start

main.yaml
security:
  events:
    to: [log, otel]
    min: low
    regions:
      eu: { endpoint: https://otel.eu.acme.dev:4318, headers: { authorization: secret:OTEL_EU_TOKEN } }
  detect:
    brute-force: { failures: 10, window: 5m }
    stuffing: { accounts: 20, window: 5m }
    probing: { denials: 20, window: 1m }
app/example.ts
import { security } from "mainsdk";

// the app's own event: only app.* types; the SDK writes the catalogue events itself
security.emit({ type: "app.refund_spike", severity: "medium", message: "Refunds above the daily norm", attrs: { count: 41 } });

// read every sign-in refusal after it is written
const stop = security.on("auth.*", (e) => console.log(e.type, e.severity));
stop();

Configuration

The security block of main.yaml sets where events go (security.events) and the detector thresholds (security.detect), as in the quick start. See the security.* rows in main.yaml reference. mainsdk check reports an invalid value as MAIN-SECURITY-0001 and an otel sink with no endpoint for a region the app stores data in as MAIN-SECURITY-0002.

What you get

  • Every event is OCSF with ATT&CK tags and no personal data (keyed user hash, trimmed IP)
  • Never on the request path; export is buffered and drops events below high first
  • Each event goes to the endpoint of its tenant's region
  • Detectors run in the app with bounded counters

Calls

CallWhat it does
security.emit({ type, severity, message, attrs?, attack? })Writes the app's own event. The type must start with app.; attributes must be in telemetry@1. A bad type is refused with MAIN-SECURITY-0005.
security.on(type, fn)Calls fn for each event of that type (auth.* and * also work) after it is written. Returns a function that stops listening. A subscriber that throws is MAIN-SECURITY-0004; the others still run.

What the SDK writes for you

The SDK writes catalogue events from the places where an attack becomes meaningful. Each is OCSF, tagged with MITRE ATT&CK (OWASP LLM ids for model events), explained in one sentence and free of personal data: actor.user is a keyed hash and actor.ip is trimmed. The hash key is random for each process, so a hash is stable inside one run of the app and meaningless outside it.

EventSeverityWritten when
auth.failedinfoA sign-in is refused
auth.brute_force, auth.stuffinghighA detector fires on sign-in refusals
access.probinghighOne actor is denied many different resources
access.cross_tenanthighAn access decision crosses a tenant
egress.refusedmediumAn outbound call is refused (MAIN-NET-0002, MAIN-NET-0003)

The detectors report; they never block. Counters are bounded, and no key is a raw id or address.

Wired today, and not yet

Wired: sign-in refusals from auth, access denials (access.decision) and refused egress.

Not all producers are called yet. These types exist in the catalogue and as the module's record function, but their call sites are not wired: auth.failed from every sign-in refusal point, inbound.csrf, inbound.cors, inbound.csp, ai.injection, canary events and seal.mismatch. Until they are, those events do not appear.

Where events go

  • log is always on: the local event store and terminal. Read it with mainsdk logs --security.
  • otel sends events at or above min to the OTLP exporter. Each event goes to the endpoint of its tenant's region (security.events.regions.<region>, else telemetry.otel).
  • events publishes security.<type> through the events interface.

Sigma rules for the detections ship with the SDK, so the same alerts can run in your SIEM. High and critical events also raise an alert.

CLI

text
mainsdk logs --security [--json] [--follow]
mainsdk info

mainsdk info shows a security line with the sinks, the lowest severity sent, the detector thresholds and event counts by type.

Fail static

Events never sit on the request path. Writing to the log is a queued step; export is buffered. If the buffer is full, events below high are dropped first and the app logs MAIN-SECURITY-0003 once a minute with the count. If an endpoint is down, the app keeps serving and keeps the log. No sink can refuse a request.

Errors and checks

  • MAIN-SECURITY-0001 and MAIN-SECURITY-0002: checks
  • MAIN-SECURITY-0003, -0004 and -0005: export buffer full, a subscriber threw, a bad security.emit
  • MAIN-SECURITY-0010, -0011 and -0012: brute force, credential stuffing, access probing

Each code's cause and fix is in check codes.

Swapping drivers

The only driver is builtin, the default, with the log, OTLP and events sinks built in. You choose where events go with security.events.to, not with a driver, and no call changes. See Where events go.

DriverProvider
builtinBuilt in (log, OTLP and events sinks)default

Testing

createTestApp() from @mainsdk/testing runs the events module with no collector. Emit an event, flush, and read it back with app.security.recent().

app/refunds.test.ts
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";

const app = createTestApp({ yaml: "schema: 1\nproject: shop\n" });
app.security.emit({ type: "app.refund_spike", severity: "medium", message: "Refunds above the daily norm", attrs: { count: 41 } });
await app.security.flush();
assert.equal(app.security.recent("app.refund_spike").length, 1);

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet