Skip to content
Docs/CITADEL Governance/AUGUR & VIGIL
Edit on GitHub

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:

MARSHAL gate sequence
Bash
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.

rule_01 condition
Go
"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.

rule_02 condition
Go
"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.

rule_03 condition
Go
"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.

Warning: rule_03's 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 statusMeaningEffect on MARSHAL outcome
PASSAll rules evaluated cleanly — no anomalies detectedNo change; outcome determined by other gates
WARNrule_01 or rule_02 fired (or both)Reason strings appended to decision.reasons[]; outcome unchanged (still EXECUTE or REFUSE from prior gates)
HARD_STOPrule_03 fired — DATA_EXPORT without incident_idOutcome 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 resultAUGUR statusFinal MARSHAL verdict
All passedPASSEXECUTE
All passedWARNEXECUTE — with AUGUR reason strings in the WORM entry
Gate 1–3 failureanyREFUSE — prior gate failure takes precedence
AnyHARD_STOPHARD_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:

  1. POST /api/v1/incidents/.../actions returns 403 ErrMarshalHardStop.
  2. If the CITADEL → IRFlow HARD_STOP webhook is configured, IRFlow receives a citadel.marshal.hard_stop event and creates a P1 incident automatically.
  3. A project-freeze runbook fires if configured on the incident type.
  4. 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:

MetricLabelsMeaning
citadel_augur_rule_fires_totalrule, statusCounter 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

ParameterDefaultWhen to change
Business-hours window (rule_01)07:00–19:00 UTCOrganisations 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 minRaise 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 alwaysDo not change — this rule exists to make the behaviour impossible.

VIGIL — Ecosystem health monitor

Planned feature — v2.0: VIGIL is fully specified and documented here so ecosystem callers can plan integration, but no VIGIL code exists in v1.0.0. Platforms publish their individual health via Prometheus metrics as usual. VIGIL is scheduled to reach 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

ColourMeaningEffect on MARSHAL (v2.0)
GREENNormal operation — all indicators within toleranceBaseline; no modification to decision outcomes
AMBERElevated risk — one or more indicators above thresholdMARSHAL appends VIGIL's colour to decision.reasons[]; outcomes unchanged
REDCritical — unresolved HARD_STOP, severe WORM lag, or chain verification failureMARSHAL 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:

SourceInput signalThreshold → state
IRFlowUnresolved P1 incident count> 0 → AMBER, > 3 → RED
IRFlowHARD_STOP events in the last hour> 0 → AMBER
IRFlowAverage age of open P1 incidents> 24 h → AMBER
CITADELWORM chain verification statusinvalid → RED (always)
CITADELAnchor signature lag> 10 min since last anchor → AMBER
CITADELWORM append failure rate (5-min window)> 1 % → AMBER
APIGuardCritical finding rate> 3 criticals / hour → AMBER
APIGuardActive scan failure rate> 10 % → AMBER
ThreatFlowHigh-confidence IOC feed gapno new feed in > 6 h → AMBER
NIS2 CompassArticle 23 notification success rate (24 h)< 90 % → AMBER
NIS2 CompassAssessment freshness age> 90 days → AMBER, > 180 days → RED

The planned endpoint response surface looks like this (v1.2+):

GET /api/v1/vigil/status — planned response
Bash
{
  "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

VersionVIGIL scope
v1.0.0 (current)Design only. No code. Platforms publish health via Prometheus metrics as usual.
v1.1Consumer-side scraping infrastructure in CITADEL — a background worker polls other platforms' /health/detail endpoints.
v1.2VIGIL computes colour state and exposes GET /api/v1/vigil/status. No MARSHAL integration yet.
v2.0Full 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.

SIN — Security Intelligence Network — Open-source cybersecurity & compliance for the EU Digital Decade.

Apache-2.0 & AGPL-3.0 · GitHub · Contribute