AUGUR & VIGIL
This page covers the two behavioural and health-monitoring subsystems of CITADEL. AUGUR is the behavioural-heuristics layer at Gate 4 of the MARSHAL engine — it asks whether a request fits the normal pattern of how the caller behaves, catching insider abuse and credential-compromise attacks that pass cleanly through the first three gates. VIGIL is the ecosystem-wide health monitor that synthesises telemetry from every SIN platform into a single colour-coded signal (GREEN / AMBER / RED).
Overview
Gates 1–3 of MARSHAL answer an authorisation question: "is this caller allowed to do this?". AUGUR at Gate 4 asks a different question: "does this action fit the normal behavioural pattern for this caller?". A credential-compromise or insider attack may clear every permission check while still exhibiting observable anomalies — unusual timing, burst frequency, or a data export with no associated investigation. AUGUR surfaces those signals before the action reaches the WORM log.
AUGUR reads from a read-only mirror database. It does not hold locks on the main write path and cannot itself delay or block WORM appends. Its influence on MARSHAL outcomes is mediated entirely through the verdict mechanism described below.
AUGUR — Gate 4 in MARSHAL
AUGUR is the fourth gate in the five-gate MARSHAL pipeline. The gate sequence is:
Request → Gate 1: AuthN (Ed25519 signature) → Gate 2: AuthZ (RBAC permission check) → Gate 3: NDS (actor ≠ verifier, SoD enforcement) → Gate 4: AUGUR (behavioural heuristics) → Gate 5: WORM (unconditional append — every request is logged)
AUGUR evaluates its rule catalogue against the incoming Kerkese (the CITADEL governance request object) and emits one of three statuses: PASS, WARN, or HARD_STOP. That status is then factored into the overall MARSHAL verdict before the WORM append at Gate 5.
Rule catalogue (v1.0.0)
Three rules ship with v1.0.0. They are intentionally conservative — an enterprise can tune thresholds through configuration, but the rule shape is stable and WORM-observable so auditors can reproduce decisions without re-running the engine. Expanding the catalogue is explicitly out of scope until v1.2, at which point a rule-as-code plugin system becomes justifiable.
rule_01 — Off-hours action
Condition: the request's ts_utc falls outside the 07:00–19:00 UTC business-hours window.
"cm">// Gate 4 — rule_01 if kerkese.TsUtc.Hour() < 7 || kerkese.TsUtc.Hour() >= 19 { reasons = append(reasons, fmt.Sprintf("AUGUR_rule_01: action initiated outside business hours(hour=%d UTC)", kerkese.TsUtc.Hour())) status = WARN }
Rationale: most legitimate operator activity happens during business hours. An action at 03:00 UTC is worth flagging even if it passes every other gate. The rule deliberately emits a WARN rather than a block — a hard block on off-hours would cause too many false positives for 24/7 operations, but the warn surface is sufficient for after-the-fact review.
Reason string: AUGUR_rule_01: action initiated outside business hours (hour=N UTC)
Future tuning: the business-hours window will be configurable via CITADEL_AUGUR_BUSINESS_HOURS_START and CITADEL_AUGUR_BUSINESS_HOURS_END environment variables in v1.1+.
rule_02 — High frequency
Condition: the same actor.user_id has logged more than 10 Kerkese evaluations in the last 5 minutes.
"cm">// Gate 4 — rule_02 count, err := store.ActionCount(ctx, kerkese.Actor.UserID, 5*time.Minute) if err == nil && count > 10 { reasons = append(reasons, fmt.Sprintf("AUGUR_rule_02: high frequency(%d actions in 5min)", count)) if status < WARN { status = WARN } }
Rationale: human operators rarely sustain more than 10 governed actions in 5 minutes. Spikes indicate either a scripted attack attempting to race past a window, or a legitimate automation that should be migrated to a service role token. The frequency counter is maintained by the WORM append path as a sliding window — no separate time-series store is required.
Reason string addition: AUGUR_rule_02: high frequency (N actions in 5min)
rule_03 — DATA_EXPORT without incident
Condition: action.type == "DATA_EXPORT" AND action.incident_id is empty.
"cm">// Gate 4 — rule_03 if kerkese.Action.Type == "DATA_EXPORT" && kerkese.Action.IncidentID == "" { reasons = append(reasons, "AUGUR_rule_03: DATA_EXPORT attempted without incident_id — HARD_STOP") status = HARD_STOP "cm">// overrides any prior WARN }
Rationale: data exfiltration without an active incident context is the single highest-risk pattern in the threat model. An attacker who has captured a live session will often attempt a bulk export first; an honest operator always ties exports to a specific investigation. This rule is the only AUGUR rule that forces a HARD_STOP outcome.
HARD_STOP threshold is intentionally non-configurable. The rule exists precisely to make DATA_EXPORT without an incident impossible — not difficult, not warned-about, impossible. Do not add a configuration path to disable it.Status severity
| AUGUR status | Meaning | Effect on MARSHAL outcome |
|---|---|---|
PASS | All rules evaluated cleanly — no anomalies detected | No change; outcome determined by other gates |
WARN | rule_01 or rule_02 fired (or both) | Reason strings appended to decision.reasons[]; outcome unchanged (still EXECUTE or REFUSE from prior gates) |
HARD_STOP | rule_03 fired — DATA_EXPORT without incident_id | Outcome forced to HARD_STOP regardless of all other gates; P1 incident auto-created; chain anchored immediately |
How heuristics influence the verdict
AUGUR's status severity maps directly onto MARSHAL's verdict semantics, but with an important distinction: WARN is advisory — it enriches the audit record and surfaces anomalies to reviewers without blocking the operation. HARD_STOP is blocking and overrides any permissive result from earlier gates.
| Prior gates result | AUGUR status | Final MARSHAL verdict |
|---|---|---|
| All passed | PASS | EXECUTE |
| All passed | WARN | EXECUTE — with AUGUR reason strings in the WORM entry |
| Gate 1–3 failure | any | REFUSE — prior gate failure takes precedence |
| Any | HARD_STOP | HARD_STOP — overrides everything, including prior EXECUTE |
The WARN advisory model is deliberate. AUGUR is designed to complement the permission model, not duplicate it. Operators reviewing a week's audit log can search for AUGUR_rule_01 to identify after-hours patterns without having been paged at the time — the signal is preserved in the WORM chain and accessible later.
Interaction with IRFlow
When AUGUR emits HARD_STOP, the downstream effect in IRFlow is automatic:
POST /api/v1/incidents/.../actionsreturns403 ErrMarshalHardStop.- If the CITADEL → IRFlow
HARD_STOPwebhook is configured, IRFlow receives acitadel.marshal.hard_stopevent and creates a P1 incident automatically. - A project-freeze runbook fires if configured on the incident type.
- On-call is paged via the incident's notification policy.
WARN statuses are audit-only: IRFlow logs them into the action's marshal_decision field but does not escalate. The warn is visible to anyone reviewing the action after the fact.
Observability
AUGUR exposes two Prometheus counters:
| Metric | Labels | Meaning |
|---|---|---|
citadel_augur_rule_fires_total | rule, status | Counter incremented each time a rule fires, labelled by rule ID and resulting status |
citadel_augur_hard_stops_total | — | Convenience counter for rule_03 HARD_STOP events specifically |
Recommended alerting rule: rate(citadel_augur_hard_stops_total[5m]) > 0 — any HARD_STOP event is per-se incident-worthy and warrants immediate review. rule_01 and rule_02 WARNs are expected background noise; alert only when they exceed 10 % of all MARSHAL decisions over a sustained window.
Tuning guidance
| Parameter | Default | When to change |
|---|---|---|
| Business-hours window (rule_01) | 07:00–19:00 UTC | Organisations operating primarily in a non-UTC timezone; shift to match local business hours. Configurable in v1.1+ via CITADEL_AUGUR_BUSINESS_HOURS_START / CITADEL_AUGUR_BUSINESS_HOURS_END. |
| High-frequency threshold (rule_02) | 10 actions / 5 min | Raise to 20–30 for organisations with heavy automation under the service role. Consider a separate threshold for service role in v1.1. |
DATA_EXPORT without incident (rule_03) | HARD_STOP always | Do not change — this rule exists to make the behaviour impossible. |
VIGIL — Ecosystem health monitor
GET /api/v1/vigil/status in v1.2, with full MARSHAL integration in v2.0.VIGIL answers one question for every platform in the opensecstack ecosystem: "Is now a safe time to perform governance-relevant work?" It synthesises telemetry from CITADEL, IRFlow, ThreatFlow, NIS2 Compass, and APIGuard into a single colour-coded health signal that MARSHAL will consult at decision time.
GREEN / AMBER / RED states
| Colour | Meaning | Effect on MARSHAL (v2.0) |
|---|---|---|
| GREEN | Normal operation — all indicators within tolerance | Baseline; no modification to decision outcomes |
| AMBER | Elevated risk — one or more indicators above threshold | MARSHAL appends VIGIL's colour to decision.reasons[]; outcomes unchanged |
| RED | Critical — unresolved HARD_STOP, severe WORM lag, or chain verification failure | MARSHAL applies VIGIL_RED_NONEMERGENCY_BLOCK: non-emergency action types are REFUSEd until RED clears. Emergency types (CONTAIN, ISOLATE, CREATE_INCIDENT) bypass the rule. |
Aggregation rule: RED dominates AMBER dominates GREEN. Any single RED input forces the overall status to RED. The individual input colours are exposed separately on the VIGIL endpoint so operators can see which dimension triggered the escalation.
Telemetry inputs
VIGIL is a consumer of telemetry, not a producer. Its planned inputs span five subsystems:
| Source | Input signal | Threshold → state |
|---|---|---|
| IRFlow | Unresolved P1 incident count | > 0 → AMBER, > 3 → RED |
| IRFlow | HARD_STOP events in the last hour | > 0 → AMBER |
| IRFlow | Average age of open P1 incidents | > 24 h → AMBER |
| CITADEL | WORM chain verification status | invalid → RED (always) |
| CITADEL | Anchor signature lag | > 10 min since last anchor → AMBER |
| CITADEL | WORM append failure rate (5-min window) | > 1 % → AMBER |
| APIGuard | Critical finding rate | > 3 criticals / hour → AMBER |
| APIGuard | Active scan failure rate | > 10 % → AMBER |
| ThreatFlow | High-confidence IOC feed gap | no new feed in > 6 h → AMBER |
| NIS2 Compass | Article 23 notification success rate (24 h) | < 90 % → AMBER |
| NIS2 Compass | Assessment freshness age | > 90 days → AMBER, > 180 days → RED |
The planned endpoint response surface looks like this (v1.2+):
{
"overall": "AMBER",
"updated_at": "2026-04-19T10:15:00Z",
"components": [
{ "name": "worm_chain", "status": "GREEN", "since": "2026-04-12T00:00:00Z" },
{ "name": "irflow_p1", "status": "AMBER", "value": 2, "threshold": "> 0", "since": "2026-04-19T08:22:00Z" },
{ "name": "nis2_success", "status": "GREEN", "value": 99.2 },
{ "name": "apiguard_crit", "status": "GREEN", "value": 1 },
{ "name": "threatflow", "status": "GREEN", "since": "2026-04-12T00:00:00Z" }
]
}Interaction with MARSHAL
In v2.0, VIGIL's output becomes an explicit input to MARSHAL gate evaluation. The interaction is designed to be in-band — VIGIL's state is available to MARSHAL at decision time, not just to a monitoring pager. Any VIGIL-driven MARSHAL outcome carries the colour state in the WORM entry, so auditors can reconstruct what VIGIL showed at the exact moment of any decision.
This design separates VIGIL from Prometheus alerting. Prometheus dashboards show per-service numbers; VIGIL combines them into a single cross-platform signal that influences governance decisions and is recorded in the immutable audit chain.
Implementation timeline
| Version | VIGIL scope |
|---|---|
| v1.0.0 (current) | Design only. No code. Platforms publish health via Prometheus metrics as usual. |
| v1.1 | Consumer-side scraping infrastructure in CITADEL — a background worker polls other platforms' /health/detail endpoints. |
| v1.2 | VIGIL computes colour state and exposes GET /api/v1/vigil/status. No MARSHAL integration yet. |
| v2.0 | Full MARSHAL rule integration (VIGIL_RED_NONEMERGENCY_BLOCK). This is the last step because it changes decision semantics for every caller and requires a careful rollout plan alongside the rule-as-code plugin system and multi-writer chain. |