Verdict-Driven Corpora
When an analyst records a disposition, the admitted detection events behind that incident are useful regression evidence. True-positive and benign-true-positive verdicts become a corpus that rule backtest can replay. False-positive verdicts become input for rule tune --from-dispositions.
The daemon does not keep those payloads on the incident summary. Verdict-driven capture is an opt-in, byte-bounded ring for detection results admitted by a group_by alert pipeline. Accepted dispositions enqueue one asynchronous bundle per original verdict. The ring is not an event store, is not queryable, and is not written to the SQLite state database.
Scope
Capture is detection-only and group_by-only.
- Correlation results are rejected and counted as
unsupported_result_kind. group.mode: entity_graphis rejected at startup because its UUID identity can merge.- If dedup is configured, every
group.byselector must also appear indedup.fingerprint. - Capture runs after inhibition and silencing, and before dedup, so muted events never become evidence while repeated admitted firings do.
Changing any daemon.capture.* key requires a restart. The setting is not hot-reloadable.
Enable
Capture is off by default. Enabling it also requires dispositions, a group_by alert pipeline, and daemon.capture.spool_dir.
daemon:
dispositions:
enabled: true
capture:
enabled: true
spool_dir: /var/lib/rsigma/capture
rsigma engine daemon -r rules/ --alert-pipeline pipeline.yml --config rsigma.yaml --enable-dispositions --enable-capture
--enable-capture turns the feature on for one run. Bounds and spool_dir stay config-file-only under daemon.capture.
When capture is enabled, the engine retains detection event payloads internally even if you did not pass --include-event. Those payloads are stripped before sink delivery unless you independently requested --include-event.
Bounds
Every limit must be greater than zero, and max_event_bytes <= max_bytes_per_incident <= max_capture_bytes. Oversized events are dropped whole. They are never truncated.
| Key | Default | Role |
|---|---|---|
max_captured_incidents |
1000 |
Open incidents held in the ring |
max_events_per_incident |
1000 |
Events retained per incident |
max_event_bytes |
1MiB |
Encoded size of one event |
max_bytes_per_incident |
16MiB |
Encoded size of one incident |
max_capture_bytes |
256MiB |
Encoded size of the whole ring |
ttl |
24h |
How long a ring generation is kept after last admission |
max_spool_bytes |
10GiB |
Completed-bundle disk budget |
spool_queue_capacity |
128 |
In-flight spool jobs |
A bundle larger than max_spool_bytes fails before commit. Completed bundles are evicted oldest-first when a new write would exceed the disk budget. Ring occupancy is independent of disk retention.
Bundle layout
One directory per original normalized disposition. The directory name is the SHA-256 hex digest of that identity, so paths never contain raw incident or fingerprint strings.
spool_dir/tp/<bundle_id>/fortrue_positiveandbenign_true_positivespool_dir/fp/<bundle_id>/forfalse_positive
Each bundle contains:
corpus/events.ndjson: bare captured event objects onlyprovenance/events.ndjson: one line per event withevent_digest,captured_at,matches, and the eventmanifest.json: format version, bundle id, kind (detection_group_by), verdict, scope, rule ids, counts, and creation timeexpectations.ymlon TP/BTP bundles only: a complete document withat_least: 1per contributing detection rule andcorpus: events.ndjson
Do not pass spool_dir itself to rule backtest. Backtest walks every file under --corpus as an event source.
Commands
Replay a true-positive bundle:
rsigma rule backtest -r rules/ --corpus /var/lib/rsigma/capture/tp/<bundle_id>/corpus --expectations /var/lib/rsigma/capture/tp/<bundle_id>/expectations.yml
Tune from every represented detection rule in the spool:
rsigma rule tune -r rules/ --from-dispositions /var/lib/rsigma/capture
--from-dispositions conflicts with --fp and --tp. It reads only versioned bundles, selects events whose provenance matches contain the target rule, and fails if a rule has false-positive evidence and no true-positive protection set. Malformed bundles fail with the file path; nothing is skipped.
The daemon never edits the rules directory. Converting a TP bundle into an embedded rsigma.exemplars block stays a human review step.
Corrections, restarts, and queue loss
Successful spooling does not remove the ring entry. It stays until TTL or ring eviction so a later corrected verdict can write a separate TP or FP bundle. A same-verdict retry is a no-op when the destination already exists.
A restart empties the in-memory ring. Verdicts on pre-restart incidents increment rsigma_capture_spool_jobs_total{result="miss"}. That is expected; raw events are not persisted to SQLite.
POST /api/v1/dispositions and pull-source ingest stay accepted even when spooling reports miss or queue_full. The ingest summary includes a capture array with queued, exists, miss, or queue_full for each original disposition that reached the spool hook.
Authorization and data at rest
Raw captured events may contain PII, secrets, tokens, or customer payloads. Treat spool_dir as sensitive data at rest. There is no built-in redaction.
On Unix, spool directories are created as 0700 and files as 0600. Staging lives under spool_dir/.staging/, paths refuse symlinks, and abandoned staging directories are removed at startup. Unsupported permission hardening is warned and documented.
When API authentication is enabled and capture is on, POST /api/v1/dispositions requires both dispositions:write and capture:write. The built-in operator role includes capture:write. Configured pull sources are trusted operator input and do not present a bearer token.
There is no API to read or search the ring or the spool.