Every 15 minutes Kodori scans your tenant's last hour of audit-log activity for behavioural signals worth a human look. Five detector kinds:
- **High-volume regulated read** — one principal read more than the threshold of regulated documents in the window. Classic exfiltration shape; severity scales with how far past the threshold the read count went. - **Cross-sensitivity burst** — one principal touched all five sensitivity tiers (public → regulated) inside the window. Strong signal of corpus enumeration (a compromised user methodically pulling every tier they can reach). - **Off-hours burst** — a user whose activity in the window is more than 5× the median of their own prior 7 days at the same wall-clock hour. Captured per-actor so a quiet workspace doesn't false-positive. - **Held-document read spike** — repeated reads of legal-hold-bound documents from one principal. Shape of "trying to exfiltrate evidence before the hold catches up." - **Agent volume spike** — an agent (API key or in-app agent loop) crossed a tool-call threshold in the window. Catches a runaway loop or a compromised key.
Each fires deduplicate within a 24-hour window — re-running on the same actor + kind updates the existing row rather than stacking duplicates. Acknowledge or dismiss decisions are durable; both emit `anomaly.acknowledged` / `anomaly.dismissed` audit events so the trail of "who decided this was real / a false positive" survives.
**Dismiss with required reason.** Clicking "Dismiss as false positive" on any anomaly row expands a form with four quick-pick reason chips: "False positive — expected business activity", "Investigated, legitimate (action documented elsewhere)", "Duplicate of an earlier anomaly already triaged", "System / agent change planned and announced". Pick a chip; it fills the note field. Edit if you want to add specifics. Confirm-dismiss is disabled until a reason exists. The note lands on the `anomaly.dismissed` event payload, so the audit chain captures WHY each dismissal happened — defensible at a future investigation. Acknowledge's note remains optional (the action you took is logged elsewhere); dismiss's note is required because the note IS the audit trail.
**Quarterly CSV export.** Click "Export CSV ↓" at the bottom of /anomalies for a 15-column compliance-ready CSV — id, kind, severity, status, actor info, occurrence_count, first/last_seen_at, decided_by + email, decided_at, decision_note, evidence_json. Owner / admin only. Optional URL query filters: `?from=YYYY-MM-DD&to=YYYY-MM-DD&status=open|acknowledged|dismissed|auto-paused`. Date filter scopes to last_seen_at so a dismissed-last-quarter signal still surfaces. 50,000-row cap with comment-row truncation marker. The decision_note column lets compliance officers prove "47 anomalies dismissed last quarter — here's exactly why each one" without hand-building from /audit filters.
**Per-tenant threshold tuning.** Defaults are 60-minute window, 25 regulated reads, 200 agent tool calls, 5 hold-deny refusals. High-volume tenants (large legal firms with 50+ paralegals) can raise the regulated-read threshold to suppress false positives; low-volume solo-practitioner tenants can lower it to catch smaller patterns. Owners + admins tune via the "Anomaly detection thresholds" section on /settings/tenant. Empty input = revert to platform default; range-validated server-side (windowMinutes 5-1440, others 1-100,000). Saving emits `tenant.anomaly-thresholds-set` on the audit chain with from/to deltas — idempotent on no-op saves. Tuning takes effect on the NEXT 15-minute sweep tick (no restart needed).
Step-up control: any **HIGH-severity AGENT** signal automatically writes a deny rule on `permissions` for that agent principal. The principal is paused — every tool call refuses until an owner / admin lifts the rule. The unpause flow on /anomalies requires a written rationale and is captured on the audit log. Users never auto-pause; the false-positive risk is too high without human judgment.
Find the queue at /anomalies (owner / admin only). The /compliance overview surfaces a banner whenever the open count crosses zero, with a stronger red banner when an agent identity is auto-paused. Click "Scan now" to invoke an immediate detector pass without waiting for the next cron tick.
The detectors are intentionally simple — count + group + threshold against a transparent SQL query. Behavioural-baseline / ML-driven detection lands behind a feature flag once we have customer activity to train against. For v1 we wanted detector logic that an auditor can read in one sitting.