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_modereports which sample kinds the incident actually retained:refs,results,mixed, ornone. Samples are retained when a result is absorbed but reported under the mode configured at read time, so changingincludewhile an incident is open leaves both kinds behind.mixedsays so instead of hiding one of them.bundle_readyisfalsewhile the incident is still insidegroup_waitand has not been reported yet. Its contents can still change, so the bundle route withholds it until this istrue.
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, anddocumentshas 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, anddocumentsis 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). |
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
- Streaming Detection for the daemon overview and hot-reload semantics.
- OTLP Integration for
/v1/logsagent recipes. - Prometheus Metrics for
/metricsdefinitions and alert recipes. - Observability for the broader
tracingand metrics story. - Processing Pipelines: dynamic pipelines for the source declarations exposed by
/api/v1/sources. - Security: TLS termination for the API listener for the optional
daemon-tlsbuild feature and the--tls-*flag set.