Excititor Quarantine Runbook

Scope

Excititor writes rejected VEX payloads to vex.excititor_quarantine when a connector boundary gate fails before raw-sink admission and VexConnectorVerificationOptions.QuarantineOnFailure=true.

Current reason families:

Reason familySourceMeaning
signature-verification:<reason>VexSignatureRequiredExceptionMissing, invalid, unsupported, or policy-rejected VEX signature.
csaf-schema-invalid:<issue-code>CsafSchemaValidationExceptionSigned CSAF payload failed the fetch-time schema gate.
aoc-lineage-brokenVexAocLineageBrokenExceptionWorker pre-consensus AOC lineage checkpoint rejected the document. Written by VexWorkerLineageCheckpoint (wired into DefaultVexProviderRunner); the per-document failure is quarantined and the batch continues with the next document.

Signature and CSAF rows are written at fetch time by VexConnectorBase (only when QuarantineOnFailure=true); aoc-lineage-broken rows are written by the worker checkpoint after the raw document has been admitted but before its claims become visible to consensus consumers. The signature and CSAF reasons carry a :<suffix> discriminator; aoc-lineage-broken is a fixed string with the lineage detail held in the row diagnostics.

Rows are tenant-isolated by PostgreSQL RLS through the app.tenant_id session variable (enforced by vex_app.require_current_tenant()). Set it with SELECT set_config('app.tenant_id', '<tenant>', false); — or use a tenant-scoped application connection — before any direct SQL inspection. If the variable is unset or empty, require_current_tenant() raises app.tenant_id session variable not set (SQLSTATE P0001) and the query fails outright — it does not silently return zero rows. The same FOR ALL policy gates INSERT/UPDATE/DELETE (WITH CHECK), so purge statements also require the session variable.

Row id is a deterministic SHA-256-derived UUIDv5-shaped GUID over (tenant, connector_id, source_uri, captured_at, digest, reason) (VexQuarantineIds.Derive). The sink writes with ON CONFLICT (id) DO UPDATE SET diagnostics, expires_at, so re-quarantining the same document does not create a duplicate row — it refreshes the diagnostics and extends expires_at.

Inspect

SELECT id,
       connector_id,
       source_uri,
       captured_at,
       digest,
       reason,
       diagnostics,
       expires_at,
       payload_size_bytes
FROM vex.excititor_quarantine
WHERE tenant = current_setting('app.tenant_id', true)
ORDER BY captured_at DESC
LIMIT 50;

Every row carries these common keys, added by VexConnectorBase.BuildQuarantineDiagnostics:

For CSAF schema failures, inspect (from CsafValidationResult.ToDiagnostics):

For signature failures, inspect (from VexConnectorBase.BuildSignatureDiagnostics):

For AOC lineage failures, inspect (from VexWorkerLineageCheckpoint.BuildQuarantineDiagnostics):

Replay

Replay must be explicit and operator-controlled. Do not copy quarantined bytes into the raw sink by SQL. There is no dedicated quarantine-replay CLI command today; replay is a manual operator procedure run through a one-off diagnostic harness that:

  1. Reads payload, source_uri, connector_id, and digest.
  2. Re-runs the same signature and schema gates with the updated trust root or validator configuration.
  3. Calls the normal connector raw sink only if all gates pass.
  4. Records a new audit entry linking the original quarantine id.

The quarantine schema (010_excititor_quarantine.sql) does not model replay: there is no replay-status column and no audit/linkage table referencing the quarantine id. Any audit trail in step 4 lives outside vex.excititor_quarantine and must be provided by the diagnostic harness, not the persistence layer.

Purge

Default retention is 30 days. For signature and CSAF rows (written by VexConnectorBase) it is configurable through VexConnectorVerificationOptions.QuarantineRetention (default TimeSpan.FromDays(30); a non-positive value falls back to 30 days). For aoc-lineage-broken rows the worker checkpoint ignores that option and always applies its own VexWorkerLineageCheckpoint.DefaultQuarantineRetention constant (30 days, not currently host-configurable). Purge expired rows per tenant:

DELETE FROM vex.excititor_quarantine
WHERE tenant = current_setting('app.tenant_id', true)
  AND expires_at < NOW();

Never purge rows required by an active incident, release hold, or external audit. If a release guide pins a longer retention period, configure the connector retention before enabling quarantine in that environment.