HTTP API

The engine daemon binds a single Axum HTTP server on --api-addr (default 0.0.0.0:9090) that handles probes, metrics, REST control endpoints, an HTTP event ingest endpoint, and (with the daemon-otlp feature) OTLP log ingestion.

All bodies are JSON unless otherwise noted. All responses include a Content-Type header. Error responses are JSON objects with an error key.

Endpoint summary

Path Method Permission Description
/healthz GET always open Liveness probe. Always 200 once the listener is up.
/readyz GET always open Readiness probe. 200 when rules and pipelines are loaded; 503 during startup or after a failed reload.
/metrics GET metrics:read Prometheus text format. See Prometheus metrics.
/api/v1/status GET status:read Counters, state-entry counts, uptime, and (when configured) dynamic-source summary.
/api/v1/correlations GET correlations:read Compiled correlation list with per-group counts. Empty when the engine has no correlation rules.
/api/v1/correlations/state GET correlations:read Live per-group correlation window snapshot (current aggregate vs threshold, window entries, last alert, seconds to eviction). Filter with ?id= and ?group=.
/api/v1/incidents GET incidents:read Open incidents from the alert-pipeline grouping stage.
/api/v1/incidents/{id} GET incidents:read One open incident, in the same shape as a list entry.
/api/v1/incidents/{id}/bundle GET incident-bundles:read One incident joined to its rules’ ADS documentation and the risk entities it overlaps, as JSON or Markdown.
/api/v1/risk GET risk:read Open entities tracked by the risk accumulator, with their window score, tactic count, source count, and window bounds.
/api/v1/silences GET, POST silences:read, silences:write List silences, or create one (returns its id).
/api/v1/silences/{id} DELETE silences:write Remove a silence by id.
/api/v1/dispositions GET, POST dispositions:read, dispositions:write Ingest analyst dispositions, or read the per-rule false-positive ratio. Disabled by default; enable with daemon.dispositions.enabled: true or --enable-dispositions.
/api/v1/rules GET rules:read Rule counts and rules-directory path.
/api/v1/reload POST reload:execute Trigger an immediate rules + pipelines reload.
/api/v1/events POST events:ingest NDJSON event ingest. Only enabled with --input http.
/api/v1/sources GET sources:read Dynamic pipeline sources currently registered.
/api/v1/sources/resolve POST sources:write Force re-resolution of all dynamic sources (with no body) or one specific source (with {"source_id":"..."}).
/api/v1/sources/resolve/{source_id} POST sources:write Force re-resolution of a single source by path parameter (no body). Equivalent to the body variant above; useful when the caller has to fit inside an HTTP client that does not send a JSON body on POST.
/api/v1/sources/cache/{source_id} DELETE sources:write Invalidate one source’s cache so the next read fetches fresh.
/api/v1/fields GET fields:read Combined gap + broken-coverage report. Requires --observe-fields.
/api/v1/fields/unknown GET fields:read Fields seen in events that no rule references. Requires --observe-fields.
/api/v1/fields/missing GET fields:read Fields referenced by rules that have never appeared in an event. Requires --observe-fields.
/api/v1/fields/observer DELETE fields:write Reset the field observer’s counters. Requires --observe-fields.
/api/v1/schemas GET schemas:read Per-schema event breakdown and unknown rate. Requires --observe-schemas.
/api/v1/schemas DELETE schemas:write Reset the schema observer’s counters and samples (including the discovery sample). Requires --observe-schemas.
/api/v1/schemas/suggestions GET schemas:read Candidate schema signatures mined from the unrecognized-event sample. Requires --discover-schemas.
/api/v1/tap GET tap:read Stream a bounded, optionally-redacted window of the live event stream as chunked NDJSON. Disabled by default; enable with daemon.tap.enabled: true.
/api/v1/detections/stream GET detections:read Stream live detections as chunked NDJSON, with optional level / rule filters. Disabled by default; enable with daemon.tail.enabled: true.
/api/v1/audit GET audit:read Paginated control-plane mutation audit log (who, what, when, outcome). Requires --state-db; auto-enabled when a state database is configured.
/v1/logs POST events:ingest OTLP/HTTP log ingestion (application/x-protobuf or application/json, optionally gzip-encoded). Requires daemon-otlp.
OTLP/gRPC LogsService/Export gRPC events:ingest OTLP over gRPC on the same --api-addr. Requires daemon-otlp.

The Permission column applies only when authentication is enabled; by default the daemon runs without authentication and every route is open. In-process TLS termination is available via the optional daemon-tls build feature: pass --tls-cert / --tls-key to terminate TLS for the HTTP REST, OTLP/HTTP, and OTLP/gRPC surfaces on the same --api-addr, and --tls-client-ca to require mTLS. See TLS termination for the API listener for the full flag set.

Authentication

Bearer-token authentication is opt-in and off by default, so loopback and trusted-network deployments are untouched. Enable it either with the --api-token-env <ENV_VAR> flag (a single token with full admin permissions, read from the named environment variable) or with the daemon.api.auth config block for per-token roles:

daemon:
  api:
    auth:
      anonymous_permissions: ["metrics:read"]
      roles:
        triage-bot: ["*:read", "silences:write", "dispositions:write"]
      tokens:
        - name: grafana
          role: reader
          token_env: RSIGMA_API_TOKEN_GRAFANA
        - name: shipper
          role: ingest
          token_env: RSIGMA_API_TOKEN_SHIPPER
        - name: ci
          role: triage-bot
          token_env: RSIGMA_API_TOKEN_CI

Clients send Authorization: Bearer <token>. Each route requires the resource:action permission in the summary table above; a token’s permission set comes from its role. The built-in roles are reader (*:read), operator (*:read plus every control-plane write except reload), ingest (events:ingest only, so a log shipper’s token cannot create silences), and admin (*). Custom roles are permission lists with * wildcards ("*:read", "silences:*"); a token can also carry an inline permissions list instead of a role.

Token secrets never live in YAML: token_env names an environment variable, resolved once at startup, and a missing or empty variable fails startup. Token comparison is constant time. GET /healthz and GET /readyz are always unauthenticated so liveness probes need no secrets; anonymous_permissions grants a permission set to requests without an Authorization header (for example ["metrics:read"] keeps Prometheus scraping token-free, and ["*:read"] protects only the mutating endpoints).

Failure semantics: a missing or unrecognized token gets 401 Unauthorized (with a WWW-Authenticate: Bearer header); a recognized token without the required permission gets 403 Forbidden naming the missing permission. A presented-but-invalid token is always 401, never a fallback to the anonymous grants. OTLP/gRPC clients pass the same authorization metadata and receive UNAUTHENTICATED / PERMISSION_DENIED gRPC status codes. Rejections are counted in rsigma_api_auth_failures_total{reason} and logged at warn level with the token name (never the secret). See Security for the threat-model discussion.

Probes

GET /healthz

Liveness probe. Returns 200 once the listener has accepted at least one accept; never returns 5xx unless the process is being killed.

curl -sS http://127.0.0.1:9090/healthz
{"status":"ok"}

GET /readyz

Readiness probe. 200 when rules and pipelines are loaded; 503 during startup or after a reload failure. Drain traffic when 503.

curl -sS http://127.0.0.1:9090/readyz
{"status":"ready","rules_loaded":true}

503 body:

{"status":"not_ready","rules_loaded":false}

Status and counters

GET /api/v1/status

Snapshot of engine counters plus uptime. The dynamic_sources block is present only when a pipeline declares sources.

curl -sS http://127.0.0.1:9090/api/v1/status
{
  "status": "running",
  "detection_rules": 1,
  "correlation_rules": 0,
  "correlation_state_entries": 0,
  "events_processed": 2,
  "detection_matches": 1,
  "correlation_matches": 0,
  "uptime_seconds": 19.07,
  "dynamic_sources": {
    "total": 2,
    "resolves_total": 4,
    "errors_total": 0,
    "cache_hits": 0
  }
}

The same counters are exposed in Prometheus form on /metrics. Use /api/v1/status for a quick one-shot snapshot; use /metrics for monitoring. For a formatted view from the command line, rsigma engine status fetches this endpoint and renders it as a table (or json/ndjson/csv/tsv).

GET /api/v1/correlations

The compiled correlation list with per-group counts (no window contents). Empty when the engine has no correlation rules.

curl -sS http://127.0.0.1:9090/api/v1/correlations
{
  "correlations": [
    {
      "index": 0,
      "title": "Many Logins",
      "type": "event_count",
      "timespan_secs": 3600,
      "group_by": ["User"],
      "rule_refs": ["login-rule"],
      "threshold": ">= 3",
      "active_groups": 1
    }
  ],
  "count": 1
}

GET /api/v1/correlations/state

The live per-group window snapshot, the live counterpart of engine eval --dump-correlation-state. Each group reports the current aggregate (got) against the threshold, whether the condition is currently met, the window entries, earliest/latest timestamps, seconds_to_eviction, the last_alert and suppression_remaining (when applicable), and the raw window state. Filter with ?id= (correlation id, name, or title) and ?group= (substring of the rendered field=value key).

curl -sS 'http://127.0.0.1:9090/api/v1/correlations/state?group=admin'
{
  "correlations": [ { "index": 0, "title": "Many Logins", "type": "event_count", "threshold": ">= 3", "active_groups": 1, "...": "..." } ],
  "groups": [
    {
      "correlation_index": 0,
      "correlation_title": "Many Logins",
      "type": "event_count",
      "group_key": [ { "field": "User", "value": "admin" } ],
      "group_key_display": "User=admin",
      "got": 2.0,
      "threshold": ">= 3",
      "met": false,
      "entries": 2,
      "timespan_secs": 3600,
      "seconds_to_eviction": 3590,
      "window": { "EventCount": { "timestamps": [1767225600, 1767225610] } }
    }
  ],
  "count": 1
}

GET /api/v1/incidents

Open incidents from the alert-pipeline grouping stage (present when --alert-pipeline configures a group block). Each entry has the same shape as an emitted IncidentResult, with state: open and trigger: snapshot.

curl -sS http://127.0.0.1:9090/api/v1/incidents
{
  "count": 1,
  "incidents": [
    {
      "incident_id": "f8bcd62a829b1126",
      "state": "open",
      "trigger": "snapshot",
      "first_seen": 1719412800,
      "last_seen": 1719412860,
      "max_level": "high",
      "result_count": 2,
      "rule_counts": {"rule-1": 2},
      "group_by": {"match.CommandLine": "malware x"},
      "refs": [{"rule": "rule-1", "level": "high"}],
      "sample_mode": "refs",
      "bundle_ready": true
    }
  ]
}

The include mode configured on the group block decides whether each incident carries lightweight refs or full results. See the Alert Pipeline guide.

Two fields appear only on snapshots, never on emitted incidents:

  • sample_mode reports which sample kinds the incident actually retained: refs, results, mixed, or none. Samples are retained when a result is absorbed but reported under the mode configured at read time, so changing include while an incident is open leaves both kinds behind. mixed says so instead of hiding one of them.
  • bundle_ready is false while the incident is still inside group_wait and has not been reported yet. Its contents can still change, so the bundle route withholds it until this is true.

GET /api/v1/incidents/{id}

One open incident, in the same shape as a list entry. Returns 404 when the id is unknown, which includes an incident that has already resolved and been evicted, and 503 when the alert pipeline has no group block.

curl -sS http://127.0.0.1:9090/api/v1/incidents/f8bcd62a829b1126

GET /api/v1/incidents/{id}/bundle

A self-contained incident report: the incident joined to the ADS documentation of every rule that contributed and the risk entities it overlaps. Requires incident-bundles:read, which is deliberately separate from incidents:read because a bundle hands out more than the incident list does.

Parameter Values Default Description
format json, markdown (md) json The rendering. JSON is served as application/json, Markdown as text/markdown; charset=utf-8.
curl -sS http://127.0.0.1:9090/api/v1/incidents/f8bcd62a829b1126/bundle
curl -sS 'http://127.0.0.1:9090/api/v1/incidents/f8bcd62a829b1126/bundle?format=markdown'
{
  "schema_version": 1,
  "generated_at": "2026-07-26T12:00:00Z",
  "sources": {"rules": true, "risk": true},
  "incident": { "incident_id": "f8bcd62a829b1126", "...": "as above" },
  "rules": [
    {
      "key": "rule-1",
      "count": 2,
      "resolution": "unique",
      "documents": [
        {
          "identity": {"kind": "detection", "id": "rule-1", "title": "Whoami execution"},
          "level": "high",
          "tags": ["attack.discovery"],
          "ads": {"sections": [{"id": "goal", "required": true, "present": true, "carrier": "description", "content": "Detects whoami execution."}]}
        }
      ]
    }
  ],
  "risk": [
    {"matched_on": "risk_object", "entity_type": "user", "entity_value": "alice", "score": 120, "...": "as GET /api/v1/risk"}
  ]
}

An incident records only a rule key per contributing result: the rule id, or the title for a rule without one. resolution reports how that key resolved against the currently loaded rule set:

  • unique: exactly one rule carries the key, and documents has one entry.
  • ambiguous: several loaded rules carry it with differing documentation, and every one is listed. A routed rule set compiles the same rule once per pipeline-set, so this appears when a pipeline rewrites a rule’s documentation for one schema but not another.
  • missing: no loaded rule carries the key, and documents is empty. Expected when the rule set changed while the incident was open.

Risk entities join on the entity type as well as its value, so the user alice is never tied to the host alice. matched_on says which evidence produced the join: risk_object when a retained result named the entity in its risk.objects enrichment, and group_key when the incident retained only references and the join came from its own grouping key. sources.risk is false when no risk accumulator is configured, which is not the same as an incident with no risk overlap.

The route returns:

Status Meaning
400 Unknown format.
404 Unknown incident id, or one that has already resolved and been evicted.
409 The incident is still inside group_wait, so its contents can still change.
503 The alert pipeline has no group block, so the daemon tracks no incidents.

The CLI wraps this route: see rsigma engine incidents export.

GET /api/v1/risk

Open entities tracked by the risk accumulator (present when --risk configures an incident block). Each entry reports the entity, its accumulated window score, the distinct ATT&CK tactic count, the distinct contributing-source count, the retained contribution count, the window bounds, and the last-fired timestamp (when the entity has fired). Empty when no risk accumulator is configured.

curl -sS http://127.0.0.1:9090/api/v1/risk
{
  "count": 1,
  "entities": [
    {
      "entity_type": "user",
      "entity_value": "alice",
      "score": 120,
      "tactic_count": 2,
      "source_count": 2,
      "result_count": 2,
      "window_start": 1719412800,
      "window_end": 1719412860,
      "last_fired": 1719412860
    }
  ]
}

See the Risk-Based Alerting guide.

GET /api/v1/silences

List operator silences (static config silences and API-created ones) with their derived state.

curl -sS http://127.0.0.1:9090/api/v1/silences
{
  "count": 1,
  "silences": [
    {
      "id": "0b6c...",
      "matchers": [{"selector": "match.CommandLine", "op": "=~", "value": "malware.*"}],
      "created_by": "ops",
      "comment": "test maintenance",
      "origin": "api",
      "state": "active"
    }
  ]
}

POST /api/v1/silences

Create a silence. The body is a JSON object with matchers (required; each {selector, op, value} where op is =, !=, =~, or !~), optional starts_at / ends_at (RFC 3339), created_by, and comment. Returns 201 with the assigned id. A missing matcher list or a bad regex returns 400. Once the dynamic-silence cap (max_silences, default 1000) is reached it returns 429; delete silences or raise the cap.

curl -sS -X POST http://127.0.0.1:9090/api/v1/silences \
  -d '{"matchers":[{"selector":"rule","op":"=","value":"noisy-rule"}],"comment":"muted"}'
{ "status": "created", "id": "0b6c..." }

DELETE /api/v1/silences/{id}

Remove a silence by id. Returns 200 when removed, 404 when no such silence exists.

curl -sS -X DELETE http://127.0.0.1:9090/api/v1/silences/0b6c...

Audit trail

Append-only log of control-plane mutations (silences, dispositions, reload, source cache invalidation, field/schema observer resets). Enabled automatically when the daemon is started with --state-db; disable or tune retention with the daemon.api.audit config block. Data-plane ingest (POST /api/v1/events, OTLP) is never recorded. Each entry stores the HTTP method, matched route pattern, bearer token name (when authentication is enabled), response status, Unix timestamp, and an optional SHA-256 hex digest of the request body. The body itself is never stored. A request whose body exceeds max_body_bytes (default 64 KiB), whether declared via Content-Length or streamed, is rejected with 413 before the handler runs, and the rejected attempt is itself recorded (with a null digest).

Auth denials (401/403) are tracked separately via rsigma_api_auth_failures_total, not in this log.

GET /api/v1/audit

Paginated read of the audit log, newest first. Query parameters: limit (default 100, max 1000), offset, since and until (Unix seconds, inclusive bounds on ts). Returns 503 when audit is disabled (no state database). Requires audit:read when authentication is enabled.

curl -sS 'http://127.0.0.1:9090/api/v1/audit?limit=50'
{
  "count": 1,
  "entries": [
    {
      "id": 42,
      "ts": 1714500000,
      "method": "POST",
      "endpoint": "/api/v1/silences",
      "token": "op",
      "payload_digest": "a1b2...",
      "status": 201
    }
  ]
}

Retention pruning runs on daemon startup and on every state-save interval (default 30s): rows older than max_age (default 720h) are deleted, then the table is trimmed to the newest max_entries (default 10000). An optional sink emits each record as a JSON line with on_full=drop backpressure.

Dispositions

The triage feedback loop. Disabled by default; both routes return 503 unless the daemon was started with daemon.dispositions.enabled: true, --enable-dispositions, or a configured daemon.dispositions.source. See the Triage Feedback Loop guide.

POST /api/v1/dispositions

Ingest one or more analyst dispositions. The body is a single JSON object, a JSON array of objects, or NDJSON. Each object carries rule_id (required for detection scope, with a title fallback), verdict (true_positive / false_positive / benign_true_positive), an optional scope (detection default, or incident with an incident_id), optional fingerprint / incident_id alert identities, an optional RFC 3339 timestamp, and optional analyst / note (note max 2048 bytes). Returns 200 with an ingest summary; a whole-body parse failure returns 400. Requires dispositions:write when API authentication is enabled.

curl -sS -X POST http://127.0.0.1:9090/api/v1/dispositions \
  -d '{"rule_id":"proc-injection","verdict":"false_positive","analyst":"alice"}'
{ "accepted": 1, "duplicate": 0, "rejected": 0 }

Redelivery is idempotent: records deduplicate on (fingerprint or incident_id, verdict, rule_id), falling back to (rule_id, timestamp, analyst). The rule_id is part of the identity key so an incident-scoped fan-out does not collapse per-rule records. An incident-scoped record with no rule_id resolves to the incident’s contributing rules through the live incident map; an unknown incident is reported in the summary’s errors and counted as rejected.

GET /api/v1/dispositions

The per-rule false-positive ratio view: the active window_seconds, numerator, and min_sample, plus a rules array (each with rule_id, the verdict counts, total, and fp_ratio, which is null until the rule reaches min_sample). This document deserializes directly as the rule scorecard --triage input. Requires dispositions:read when API authentication is enabled.

curl -sS http://127.0.0.1:9090/api/v1/dispositions
{
  "window_seconds": 2592000,
  "numerator": "fp_only",
  "min_sample": 5,
  "rules": [
    { "rule_id": "proc-injection", "true_positives": 8, "false_positives": 1, "benign_true_positives": 0, "total": 9, "fp_ratio": 0.111 }
  ]
}

GET /api/v1/rules

Returns rule counts and the configured rules path. Useful for quickly confirming a reload picked up the expected number of rules.

curl -sS http://127.0.0.1:9090/api/v1/rules
{
  "detection_rules": 22,
  "correlation_rules": 2,
  "rules_path": "/etc/rsigma/rules"
}

Reload

POST /api/v1/reload

Trigger a full reload: rules, pipelines, and dynamic source state. Equivalent to SIGHUP or to a file change inside the watched rules directory. The body is ignored.

curl -sS -X POST http://127.0.0.1:9090/api/v1/reload
{"status":"reload_triggered"}

The actual reload runs asynchronously; check /readyz and rsigma_reloads_total to confirm it completed. On a failure (parse error in a new rule), the daemon keeps serving the previously-loaded rules and increments rsigma_reloads_failed_total.

Event ingest (HTTP mode)

POST /api/v1/events

Active only when the daemon was started with --input http. Accepts NDJSON in the request body. Each line is parsed as a JSON object and queued for evaluation. Returns the number of accepted events.

curl -sS -X POST http://127.0.0.1:9090/api/v1/events \
  -H 'Content-Type: application/x-ndjson' \
  --data '{"CommandLine":"whoami /priv"}
{"CommandLine":"echo hello"}'
{"accepted":2}

Lines that fail to parse increment rsigma_events_parse_errors_total and are dropped silently. To inspect parse errors, scrape /metrics or watch the daemon’s stderr log.

Dynamic pipeline sources

GET /api/v1/sources

Lists every dynamic source registered by the loaded pipelines, with its type, refresh policy, and required flag.

curl -sS http://127.0.0.1:9090/api/v1/sources
{
  "sources": [
    {
      "source_id": "ip_blocklist",
      "pipeline": "dynamic_test",
      "type": "Http",
      "refresh": "Interval(300s)",
      "required": true
    },
    {
      "source_id": "field_config",
      "pipeline": "dynamic_test",
      "type": "File",
      "refresh": "Once",
      "required": true
    }
  ]
}

When no pipelines declare sources:

{"sources":[]}

POST /api/v1/sources/resolve

Force re-resolution of every dynamic source (with no body) or one named source (with a JSON body):

curl -sS -X POST http://127.0.0.1:9090/api/v1/sources/resolve
{"status":"resolve_triggered"}
curl -sS -X POST http://127.0.0.1:9090/api/v1/sources/resolve \
  -H 'Content-Type: application/json' \
  --data '{"source_id":"ip_blocklist"}'

If no dynamic sources are configured:

{"error":"no dynamic sources configured"}

POST /api/v1/sources/resolve/{source_id}

Force re-resolution of one named source via a path parameter, with no request body. Equivalent to the body variant of POST /api/v1/sources/resolve and useful for clients that cannot send a JSON body on POST (some load balancers, the simplest curl --data '' recipes, etc.).

curl -sS -X POST http://127.0.0.1:9090/api/v1/sources/resolve/ip_blocklist
{"status":"resolve_triggered","source_id":"ip_blocklist"}

Returns 404 {"error":"no dynamic sources configured"} when no sources are registered, and 429 {"status":"resolve_already_pending"} if a refresh for the same source_id is still in flight.

DELETE /api/v1/sources/cache/{source_id}

Invalidate the cached value for one source so the next refresh fetches fresh. Useful when an upstream feed regenerates content out-of-band of its declared TTL.

curl -sS -X DELETE http://127.0.0.1:9090/api/v1/sources/cache/ip_blocklist
{"status":"invalidated","source_id":"ip_blocklist"}

The endpoint returns 200 OK for any source ID regardless of whether that ID is currently configured; nonexistent IDs are a no-op. If you need a strict check, list /api/v1/sources first and confirm the source is registered before invalidating.

Field observability

The daemon can record the field keys of every event it evaluates and join that against the field names referenced by loaded rules. This surfaces two halves of detection coverage from inside the process:

  • Gap signal: fields in events that no rule references. Likely candidates for new detections, or a sign that an enricher should drop the field before ingestion.
  • Broken-coverage signal: fields referenced by rules that have never appeared in an event. Either the rule is dead-lettered (wrong pipeline mapping, wrong logsource) or the event source has stopped emitting that field.

Field observation is off by default. Start the daemon with --observe-fields (and optionally --observe-fields-max-keys <N>, default 10000) to enable the surface. When disabled, all four endpoints below return 503 Service Unavailable with {"error":"field observation disabled","hint":"..."}.

Three Prometheus surfaces refresh on every /metrics scrape (and after every successful /api/v1/fields/* call): rsigma_fields_observed_total, rsigma_fields_observer_unique_keys, and rsigma_fields_observer_overflow_dropped_total. See Prometheus metrics for the catalog entries.

GET /api/v1/fields

One-shot snapshot bundling summary, unknown, and missing sections. Useful for dashboards that want all three views in a single round-trip. Each list section is paginated via ?limit=N&offset=M.

curl -sS 'http://127.0.0.1:9090/api/v1/fields?limit=10'
{
  "summary": {
    "events_observed": 1248,
    "unique_keys_observed": 18,
    "rule_fields_loaded": 22,
    "overflow_dropped": 0,
    "max_keys": 10000,
    "uptime_seconds": 312.4,
    "intersection_count": 12,
    "unknown_count": 6,
    "missing_count": 10
  },
  "unknown": {
    "items": [{"field": "src_ip", "count": 1187}],
    "total": 6,
    "offset": 0,
    "limit": 10,
    "next_offset": null
  },
  "missing": {
    "items": [{
      "field": "ProcessGuid",
      "rule_count": 3,
      "sources": ["detection"],
      "rule_titles": ["Sysmon Process Tampering", "..."],
      "truncated": false
    }],
    "total": 10,
    "offset": 0,
    "limit": 10,
    "next_offset": null
  }
}

GET /api/v1/fields/unknown

Event field paths that the observer has seen but no loaded rule references. Sorted by descending count, then ascending name. Paginated with ?limit=N&offset=M.

curl -sS 'http://127.0.0.1:9090/api/v1/fields/unknown?limit=5'
{
  "items": [
    {"field": "src_ip", "count": 1187},
    {"field": "User", "count": 1183}
  ],
  "total": 6,
  "offset": 0,
  "limit": 5,
  "next_offset": null
}

GET /api/v1/fields/missing

Field names referenced by loaded rules that have never appeared in an event since the observer was started (or last reset). Each entry includes rule_count (total rules touching the field), sources (the kinds the field originated in: detection, correlation, filter, metadata), and rule_titles (up to 10 sample titles, with truncated: true when more exist).

curl -sS 'http://127.0.0.1:9090/api/v1/fields/missing?limit=5'
{
  "items": [
    {
      "field": "ProcessGuid",
      "rule_count": 3,
      "sources": ["detection"],
      "rule_titles": ["Sysmon Process Tampering"],
      "truncated": false
    }
  ],
  "total": 10,
  "offset": 0,
  "limit": 5,
  "next_offset": null
}

DELETE /api/v1/fields/observer

Clear the observer’s counters and overflow tally, and reset the per-observer uptime clock. Returns what was cleared so dashboards can subtract baselines.

curl -sS -X DELETE http://127.0.0.1:9090/api/v1/fields/observer
{"status":"reset","previous_keys":18,"previous_events":1248}

A DELETE does not affect rule loading or any other daemon state. Use it after a rule reload to start a clean coverage window against the updated rule set.

Schema observability

Available when the daemon is started with --observe-schemas. Every event is classified by schema (content-based recognition: ECS, Sysmon, rendered Windows Event Log, CEF, OCSF, a generic_json fallback, plus any --schema-config signatures), so a mixed stream’s composition and its unknown rate are visible at a glance. See engine classify for the one-shot equivalent and the signature format.

GET /api/v1/schemas

Returns the per-schema counts and the classified/unknown totals since daemon start. Returns 503 when --observe-schemas is off.

curl -sS http://127.0.0.1:9090/api/v1/schemas
{
  "summary": {
    "events_observed": 1248,
    "classified": 1203,
    "unknown": 45,
    "ambiguous": 0,
    "uptime_seconds": 612.4
  },
  "by_schema": [
    {"schema": "ecs", "count": 900},
    {"schema": "sysmon", "count": 250},
    {"schema": "generic_json", "count": 53}
  ],
  "unknown_shapes": [
    {"keys": ["deviceModel", "vendorField", "widgetId"], "count": 45}
  ],
  "routing_pruning": [
    {"schema": "sysmon", "eligible": 120, "pruned": 380}
  ]
}

unknown_shapes is a bounded, redacted sample of the field-key sets (key names only, never values) of unknown events, so you can author a signature for what is unrecognized. routing_pruning is the per-schema eligible-versus-pruned rule count, present when schema routing and logsource routing are both active. The same signals are exposed as the rsigma_events_by_schema_total{schema}, rsigma_events_unknown_schema_total, rsigma_events_ambiguous_schema_total, and rsigma_schema_rules_eligible{schema} / rsigma_schema_rules_pruned{schema} Prometheus metrics. A rising unknown rate flags a source whose schema RSigma does not recognize; add a signature with --schema-config.

GET /api/v1/schemas/suggestions

Mines the daemon’s unrecognized-event sample into candidate schema signatures, the live equivalent of engine discover-schemas. Requires --discover-schemas (which implies --observe-schemas); returns 503 when that sampler is off. Because the sample is keys-only (values are never retained), proposals use presence predicates and are tagged source: keys-only; run the offline command over a corpus for value markers.

curl -sS http://127.0.0.1:9090/api/v1/schemas/suggestions
{
  "summary": {"events_mined": 320, "shapes": 4, "clusters": 2, "candidates": 2},
  "candidates": [
    {
      "name": "discovered_devicevendor",
      "specificity": 60,
      "source": "keys-only",
      "support": 210,
      "coverage_of_unknown": 0.66,
      "predicates": ["field_present: deviceVendor", "field_present: signatureId"],
      "sample_field_sets": [["deviceVendor", "signatureId", "src"]],
      "overlap_warnings": []
    }
  ],
  "signatures_yaml": "schemas:\n  - name: discovered_devicevendor\n    specificity: 60\n    match:\n      - field_present: deviceVendor\n      - field_present: signatureId\n"
}

The rsigma_unknown_schema_clusters gauge tracks how many distinct schemas discovery would propose. Review, rename, and refine the signatures_yaml before committing it to a --schema-config file.

DELETE /api/v1/schemas

Reset the schema observer, clearing the per-schema counts, the unknown-shape sample, and the discovery sample. The discovery sample is capped and not a reservoir, so on a long-running daemon it eventually stops admitting genuinely new shapes; a DELETE refreshes it without a restart. Returns 503 when --observe-schemas is off.

curl -sS -X DELETE http://127.0.0.1:9090/api/v1/schemas

Live event tap

GET /api/v1/tap

Stream a bounded window of the live event stream as chunked NDJSON, one event per line, followed by a summary record. The capture ends at duration or limit, whichever comes first, and a dropped client connection tears the session down automatically. The capture is lossy by design: a full per-session buffer drops events (counted in the summary) rather than ever applying backpressure to the engine. This is the endpoint behind rsigma engine tap.

Disabled by default (the tap exfiltrates raw events). Enable it with daemon.tap.enabled: true or the --enable-tap flag; otherwise the endpoint returns 503 Service Unavailable with {"error":"event tap disabled","hint":"..."}.

Query param Default Description
duration 30s Capture window (humantime). Rejected with 400 above daemon.tap.max_duration (default 5m).
limit unset Stop after N events, before the duration if reached first.
stage decoded decoded (post-parse, post-filter) or raw (the input line as received).
redact unset Comma-separated dotted field paths, redacted server-side before the data leaves the daemon.
curl -sS -N 'http://127.0.0.1:9090/api/v1/tap?duration=10s&redact=user.email,src_ip'
{"CommandLine":"whoami","src_ip":"rsigma:redacted:cfea2addbf5c8284","user":{"email":"rsigma:redacted:509efebfb0e7ac1e"}}
{"CommandLine":"id","src_ip":"rsigma:redacted:8e1b...","user":{"email":"rsigma:redacted:1f9c..."}}
{"rsigma_tap_summary":{"captured":2,"dropped":0,"duration_ms":10000,"stage":"decoded"}}

Redaction is server-side. Raw values for redacted fields never cross the wire. Each value is replaced with a deterministic per-session token (rsigma:redacted:<16 hex>), so equal values map to equal tokens within one capture (preserving correlation cardinality on replay) while a random per-session salt blocks dictionary reversal and cross-fixture linkage. Paths use the same object-key / numeric-index navigation as the enrichment template engine, except a non-numeric segment meeting an array fans out to every element (fail-closed).

Error semantics:

Status When
400 Bad Request Malformed params, an invalid stage, or a duration over daemon.tap.max_duration.
409 Conflict The concurrent-session cap (daemon.tap.max_sessions, default 2) is reached.
503 Service Unavailable The tap is disabled (the default; not enabled via daemon.tap.enabled: true or --enable-tap).
The tap exfiltrates raw events

Anyone who can reach this endpoint can read live event traffic. It is off by default; enable it only behind mTLS and redact sensitive fields. See Security.

Four Prometheus metrics track the tap: rsigma_tap_sessions_total, rsigma_tap_active_sessions, rsigma_tap_events_streamed_total, and rsigma_tap_events_dropped_total. See Prometheus metrics.

Live detection tail

GET /api/v1/detections/stream

Stream live detections as chunked NDJSON, one result per line, followed by a summary record. The capture ends at duration or limit, whichever comes first; with neither it streams until the client disconnects. Each line is the same EvaluationResult shape the sinks emit (so engine tail and a saved sink file are the same format), captured after post-evaluation enrichment and before dispatch, regardless of which sinks are configured. The stream is lossy by design: a full per-session buffer drops detections (counted in the summary) rather than ever backpressuring the sink task. This is the endpoint behind rsigma engine tail.

Disabled by default. Enable it with daemon.tail.enabled: true or the --enable-tail flag; otherwise the endpoint returns 503 Service Unavailable with {"error":"detection tail disabled","hint":"..."}.

Query param Default Description
duration unset Capture window (humantime). Unset streams until the client disconnects.
limit unset Stop after N detections, before the duration if reached first.
level unset Minimum severity (informational, low, medium, high, critical); lower or unleveled results are excluded.
rule unset Case-insensitive substring matched against the rule title or id.
curl -sS -N 'http://127.0.0.1:9090/api/v1/detections/stream?level=high&rule=whoami'
{"rule_title":"Whoami Detector","rule_id":"...","level":"high","tags":[],"matched_selections":["selection"],"matched_fields":[{"field":"CommandLine","value":"whoami"}]}
{"rsigma_tail_summary":{"streamed":1,"dropped":0}}

Error semantics:

Status When
400 Bad Request Malformed params or an invalid level.
409 Conflict The concurrent-session cap (daemon.tail.max_sessions, default 2) is reached.
503 Service Unavailable The tail is disabled (the default; not enabled via daemon.tail.enabled: true or --enable-tail).

Two Prometheus metrics track the tail: rsigma_tail_active_sessions and rsigma_tail_detections_dropped_total. See Prometheus metrics.

OTLP ingest

POST /v1/logs (HTTP)

OTLP log ingestion over HTTP. Accepts application/x-protobuf or application/json, optionally gzip-encoded. Returns application/x-protobuf (or application/json matching the request) with the standard OTLP ExportLogsServiceResponse. Requires the daemon to be built with daemon-otlp.

gRPC LogsService/Export

The same OTLP gRPC service binds on the same --api-addr. Use grpcurl or any OTLP client to publish:

grpcurl -plaintext -d @ rsigma.internal:9090 \
    opentelemetry.proto.collector.logs.v1.LogsService/Export \
    < logs.json

See OTLP Integration for full agent recipes (Grafana Alloy, Vector, Fluent Bit, OpenTelemetry Collector) and the LogRecord-to-rsigma field mapping.

See also