Chio/Docs
LOGIN · JOIN

Detector health

Report a detector that cannot see as a signed health event, and suppress automatic response until its view of events is trustworthy again.

Concept chio@5f63e6e

On this page

Zero findings can come from a quiet workload or from a detector that could not see. Only the blind detector leaves a signed health receipt and holds automatic response for the scope it missed.

A detector depends on trusted input, durable history, bounded memory, and a reachable evidence store. When one of them fails, a missing finding describes missing knowledge instead of an uneventful workload. The correlator records that failure as typed health evidence, which gives the operator a repair target where a dashboard would suggest tuning the rule.

Detector healthChoose an outcome.
integrity errorsignssuppressedCorruptEventeverified eventgcorrelatorretained stateρhealth receiptκautomatic responseintegritysuppressedCorruptEventeverified eventgcorrelatorstateρhealth receiptκresponse

A frame that fails its own hash suppresses response

The outcome is Suppressed with CorruptEvent evidence and no executable finding. The health receipt is signed before the outcome is reported, and the event's delivery fails with an integrity error. Security events.

One verified event at the temporal correlator: its delivery, the record the correlator signs, and whether automatic response may proceed for the affected scope. Three of the six health kinds are drawn; CorruptState, StoreConflict, TruncatedScan, and the response scheduler's own health are left out.

A health outcome suppresses response and is signed like a finding

A health outcome carries no finding. The correlator sets its status to Suppressed and its automatic_response_suppressed flag to true, so a response plan can never treat an unhealthy detector's silence as a clean scope.

attest_detector_health signs the health evidence and appends it as a receipt, under the wire schema detector-health-receipt-body-v1, before the outcome is reported. The suppression carries the same durable, signed record a finding would, and the receipt explains why the correlation contract was incomplete.

Suppression leaves the incident itself open. Restrictions already in force keep their own owners, and missing history may still need investigation. Repairing the detector restores its ability to decide; lifting a containment is a separate act with its own owner.

Six health kinds name what the detector could not know

Every kind suppresses automatic response, and each calls for a different repair. The detector records them separately from findings:

crates/security/chio-security-types/src/event.rs254–261rust
pub enum DetectorHealthKind {
    CorruptEvent,
    CorruptState,
    StateOverflow,
    StoreConflict,
    StoreUnavailable,
    TruncatedScan,
}
KindConditionDelivery errorRepair
CorruptEvent
An event failed to parse or to hash to a group key
integrity
Fix serialization or binding; keep the input and producer identity
CorruptState
A retained partition or capacity record failed validation
integrity
Restore a consistent store from its integrity evidence
StateOverflow
The rule's state budget is exhausted
none
Compare capacity with workload; narrow or rebudget the rule
StoreConflict
A concurrent writer conflicts on every compare-and-swap retry
conflict
Reconcile writer ownership and the committed version
StoreUnavailable
The state store cannot be reached
unavailable
Restore store access, then validate retained state
TruncatedScan
A history scan stopped before its requested range ended
unavailable
Recover a complete scan or a demonstrably complete scope

A failure therefore surfaces up to three ways: a signed receipt and a suppression for every kind, a failed delivery for five of the six, and, when authorized response work later stalls, the scheduler's own page. A StoreUnavailable condition needs storage recovery, a StateOverflow needs a look at configured capacity and workload, and a TruncatedScan means the observed history is incomplete.

An ordinary event stays quiet; a corrupted frame is suppressed

The same credential-then-egress rule ignores an ordinary tool invocation and stays healthy, then suppresses automatic response when an input frame's claimed body hash no longer matches its bytes. Both cases run against the correlator and a fresh SQLite store.

Use the signed-observation fixture and the credential-then-egress rule module as security_event_cases and temporal_cases, then call compare_detector_health(path) with a fresh, private database. The corruption is a control at the correlator's dependency boundary; in a normal deployment, authenticated ingress refuses changed source evidence before this point.

Rust hostrust
detector_health_cases::compare_detector_health(&path)?;
Output
QUIET evaluated event: zero findings, zero health failures, response not suppressed
SUPPRESS corrupt frame: CorruptEvent evidence; zero executable findings

You should see zero findings on both lines, with two different meanings. The ordinary invocation is Accepted with no health failures. The corrupt frame has Suppressed status, a true suppression flag, and CorruptEvent evidence, so a dashboard that shows only the finding count hides the difference. Keep the original event ID, rule hash, affected group binding, and event-time watermark beside the health result.

The complete detector_health_cases module
security-host/detector_health_cases.rsrust
use crate::security_event_cases::{fixture_verifier, signed_observation, OBSERVATION_TIME};
use crate::temporal_cases::credential_egress_rule;
use chio_control_plane::security::AttestedCorrelationWriter;
use chio_core_types::Keypair;
use chio_quarantine::{
    CorrelationOutcome, CorrelationPolicy, CorrelationStatus, TemporalCorrelator,
};
use chio_security_types::ports::{Digest32, PortError, PortResult, SecurityEventVerifierPort};
use chio_security_types::{DetectorHealthKind, SecurityEventKind};
use chio_store_sqlite::security_state::SqliteSecurityStateStore;
use std::{path::Path, sync::Arc};

/// Controlled corrupt-frame comparison at the correlator dependency boundary.
/// Normal authenticated ingress refuses altered source evidence before this point.
pub fn compare_detector_health(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
    if path.exists() {
        return Err("health comparison database must be fresh".into());
    }
    let producer = Keypair::generate();
    let verifier = fixture_verifier(&producer)?;
    let event = verifier.verify(&signed_observation(
        &producer,
        "ordinary-read",
        "review-1",
        SecurityEventKind::ToolInvocation,
        OBSERVATION_TIME,
    )?)?;
    let rule = credential_egress_rule()?;
    let detector = TemporalCorrelator::new(
        Arc::new(SqliteSecurityStateStore::open(path)?),
        CorrelationPolicy::new(0, 4_096, 3, false)?,
    );
    let quiet = detector.ingest(&rule, &event);
    assert_eq!(quiet.status, CorrelationStatus::Accepted);
    assert!(quiet.findings.is_empty() && quiet.detector_health.is_empty());
    assert!(!quiet.automatic_response_suppressed);
    let mut corrupt = event.clone();
    corrupt.body_hash = Digest32::new([0; 32]);
    let failed = detector.ingest(&rule, &corrupt);
    assert_eq!(failed.status, CorrelationStatus::Suppressed);
    assert!(failed.automatic_response_suppressed && failed.findings.is_empty());
    assert!(failed
        .detector_health
        .iter()
        .any(|h| h.kind == DetectorHealthKind::CorruptEvent));
    println!("QUIET evaluated event: zero findings, zero health failures, response not suppressed");
    println!("SUPPRESS corrupt frame: CorruptEvent evidence; zero executable findings");
    Ok(())
}

/// The configured writer owns the actual signed receipt sink and finding
/// authority. An unhealthy result must be retained as health, never a finding.
pub fn retain_suppression(
    writer: &AttestedCorrelationWriter,
    original: &CorrelationOutcome,
) -> PortResult<()> {
    if original.status != CorrelationStatus::Suppressed
        || !original.automatic_response_suppressed
        || original.detector_health.is_empty()
    {
        return Err(PortError::invalid_data());
    }
    let executable_findings = writer.attest_outcome(original)?;
    if !executable_findings.is_empty() {
        return Err(PortError::integrity_failure());
    }
    Ok(())
}

Health is retained before response resumes for the scope

retain_suppression passes the suppressed outcome to the configured writer, which validates the health scope and policy, signs the detector-health receipt, and appends it to the host's receipt sink. It uses the host's finding authority and policy-hash registry, and its returned vector holds no executable findings. An append error stays an error, and a missing receipt is never read as evidence of a healthy detector.

Keep the affected scope suppressed while you repair the owner, and preserve the rejected or incomplete history. Resume automatic response for the scope only after this check holds:

  • CorruptEvent. The original valid neighbor passes verification and correlation.
  • StoreUnavailable or StoreConflict. Retained committed state validates under the serving owner.
  • StateOverflow or TruncatedScan. The selected history is complete under explicit limits.
  • CorruptState. The original rule, version, and history agree, with no empty replacement store.

A detector's watermark is its own record of event time

The health evidence's watermark, DetectorWatermarkEvidence, records the event time the correlator has processed, or notes that the stored record contradicts it. A signed watermark is a different object: a planted artifact that only an attacker should touch.

Read a detector-health watermark as correlator bookkeeping and a signed watermark observation as deception evidence. The two have separate types and separate wire schemas.

Receipt validation and port error shapes

The receipt body's own validation, ActiveDefenseReceiptBody::validate, refuses a detector-health body as ActiveDefenseReceiptValidationError::InvalidDetectorKnowledge when its group binding is unresolved but its watermark is not Unknown, when its watermark is Contradictory and its claimed timestamp string fails to parse, or when a Contradictory watermark either names a health kind other than CorruptState or claims a timestamp that would otherwise pass as a valid committed time. It refuses a scheduler-health body recording zero attempts as ZeroSchedulerAttempts. A caller that validates a body directly receives these typed errors; deserializing a body runs the same validation and reports a failure as a serde error message.

The control plane's attestation APIs refuse the same conditions first, with a generic PortError. attest_detector_health checks the health evidence before it builds a receipt body, so a caller of AttestedCorrelationWriter::attest_outcome receives PortError::invalid_data(). NativeSchedulerHealthPort::page_once refuses a page request recording zero attempts as PortError::integrity_failure() before it signs a receipt. A caller of either path matches on the error's kind, InvalidData or IntegrityFailure.

Scheduler health covers response work that stalls after authorization

A correlated finding acts only through a response, and the scheduler that retries a stalled dispatch has its own failure mode. Its policy sets an operator_page_threshold_ms, and SchedulerPolicy::validate refuses a policy whose threshold does not exceed its max_backoff_ms as SchedulerError::InvalidPolicy.

Once the time since a retry's first failure exceeds that threshold, the scheduler assigns a health event and pages an operator, writing a signed scheduler_health receipt first. That failure belongs to dispatch and is reported apart from the six detector kinds.

Detector repair answers whether correlation can decide again; scheduler health answers whether authorized response work can complete or be restored. A healthy correlator leaves an existing containment contribution in place, so inspect the original action and its per-effect state in Quarantine and recovery.

Next: collect the evidence behind a suppressed scope