> For the complete documentation index, see [llms.txt](https://help.resouris.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.resouris.com/part-2-running-resouris/audit-and-compliance.md).

# Audit and compliance

Every meaningful action in ReSouris writes an audit event. The chain is append-only, hash-linked, and nightly-sealed with a signed daily summary — the same tamper-evidence pattern regulators expect from banking, healthcare-EMR, and telco billing systems.

## The audit\_events table

One row per event. Key columns:

* `id` — UUID primary key.
* `tenant_id` — enforces multi-tenant isolation via RLS.
* `occurred_utc` — when the event happened.
* `actor_id` — the user that took the action; NULL for scheduled jobs.
* `actor_role` — the role the actor held when they took it.
* `action` — the event kind, e.g. `roster.published`, `leave.request.decided`, `licence.installed`, `users.export.produced`, `users.import.committed`, `room_sl.pattern.replaced`. Freeform strings organised by hierarchical convention; not a closed set.
* `target_type` / `target_id` — what the action was taken on.
* `outcome` — closed set: `success | denied | error`.
* `correlation_id` — groups the events emitted by one request or job run.
* `payload` — `jsonb`, the event-specific detail.
* `prev_hash` / `row_hash` — the hash chain links (see below).

Every write path in the codebase that changes state emits one. `test_perms_seeded_match_spec.py` and the `contracts/audit.py` module enforce that permissions map to audit-emission sites.

### Event families

The audit chain uses hierarchical action strings so operators can filter by feature area. Recent phases added:

* **User export + import (v1.6.3, P28)** — `users.export.produced`, `users.export.dsar_produced`, `users.import.uploaded`, `users.import.previewed`, `users.import.committed`, `users.import.aborted`. Emitted by the per-user export UI + the three-step import wizard. Payload carries `batch_id` (for the import events), `subject_id`, `mode`, and a signature prefix so an audit reviewer can correlate an export with a later import.
* **Room service-line scheduling (v1.6.4, P29)** — `room_sl.pattern.replaced`, `room_sl.override.set`, `room_sl.override.cleared`. Emitted from within the same transaction as the write per F9.
* **HCS Job Planning approval (v2.0, P42)** — `job_plan.signoff_recorded` (record + revoke on the triple-signoff cascade), `job_plan.spa_minimum_violated` (Guidelines §5.1 Table 2 check fires regardless of override — compliance trail), `team_job_plan.published`, `job_plan.state_changed` (draft → active → superseded lifecycle).
* **HCS Job Planning operational completeness (v1.7.4, P43)** — `job_plan.review_due` (nightly review notifier + resolved line manager), `job_plan.spa_drift_detected` (weekly runtime drift cron), `job_plan.appeal_sla_breached` (daily appeal SLA cron per Guidelines §6.11). Payload shapes documented in `v1.1/docs/security/security-v1.1.md §3.4`.

Filter the audit page by these action prefixes to see all activity for a feature area.

## The hash chain

Each row carries two hashes. `prev_hash` is the preceding row's `row_hash` for the same tenant; `row_hash` is:

```
sha256(prev_hash || canonical_bytes(this_event))
```

`canonical_bytes` is RFC 8785 (JCS) over the **whole** event — `tenant_id`, `actor_id`, `actor_role`, `action`, `target_type`, `target_id`, `outcome`, `correlation_id`, `occurred_utc` and `payload`. Changing any one of them breaks the chain, not just changing the payload.

Retroactive edit of any row breaks the chain from that row forward — subsequent rows' `row_hash` values no longer verify. This is detection, not prevention: a database admin with `UPDATE` privileges can still edit, but the tamper is visible.

> ⚠️ **THE TABLE CARRIES A SECOND, WEAKER PAIR, AND IT IS NOT THE ONE THAT MATTERS.** `event_hash` / `prev_event_hash` are maintained by a database trigger and their digest covers the **payload only**. The daily verification deliberately does **not** use them: under that wiring somebody with `UPDATE` could flip an `outcome` from `denied` to `success` and the chain would still report intact, because `outcome` is not in the digest. The operational check is the strong chain described above, over the canonical bytes of the whole event. If you are auditing the schema and see both, that is which is which.

The chain is per-tenant. Verification runs daily via `bin/rostaire-audit-chain-verify` and can be run on demand per tenant with `bin/rostaire-audit-verify`; results surface on the chain-status page below.

## The nightly seal

Daily at 02:00 UTC, the audit-seal job:

1. Reads each tenant's chain summary as at end of day: the chain head hash, the event count, and the first and last event ids.
2. Canonicalises `{tenant_id, day_utc, chain_head_hash, count_events, first_event_id, last_event_id}` as RFC 8785 JSON and takes the `sha256` of those bytes.
3. Signs that digest with the `R-AUDIT-SEAL-2026` subkey via the configured KMS adapter.
4. Writes to `audit_seals` (append-only per MIG-014's trigger) and to the witness backend.

Days on which a tenant recorded no events are left unsealed by design.

Seals are what push the audit chain from "internally consistent" to "externally verifiable." A regulator can verify seal signatures against the trust anchor without trusting the database or the operator.

**What the seal does and does not commit to.** It signs the chain *head*, not each event individually. Because every row's `row_hash` folds in its predecessor, sealing the head transitively covers every event up to that point — but it does so *through* the chain, so verifying an event against a seal means verifying the chain as well. There is no Merkle tree here: you cannot produce a compact inclusion proof for one event without the rows between it and the head.

## Witness backend

The seal-write is fronted by a **witness** backend. In v1 this is `LocalFs` — the seal is copied to a local filesystem path with a guarantee that the platform runtime cannot delete or modify it (via filesystem-level immutability where the OS supports it).

S3 Object Lock is a fail-loud stub in v1 (graduates in v1.5+). The design is deliberate: we ship the honest signal rather than a silent fallback that pretends to be more durable than it is.

## Chain-status page

Sidebar → **Assurance** → **Audit-chain status** (`/ui/admin/audit-chain-status`, gated on `audit.read`). It reports **verification runs**, not seals:

* **Latest verification** — when the run started and finished, rows examined, rows verified, whether the chain was intact, whether it alerted, and the id of the first tampered row if there was one.
* **Trend** — the same per-run figures over the requested window, with intact and tamper run counts. The window is the `days` query parameter: 30 by default, clamped to 1–365.

Seal verification is not on this page. It runs from the CLI: `bin/rostaire-witness-verify` reconciles each `audit_seals` row against its off-system witness copy byte-for-byte, and exits non-zero on any mismatch.

## Redaction

PII in audit payloads is redacted at write time. The `contracts/audit.py` module defines allowed field shapes; the `_redact_payload` helper strips fields matching any of the tenant's redaction rules. If redaction fails (e.g. a payload doesn't conform to any known shape), the write emits an alerted event with `outcome='error'` and a stub payload — the honest signal is that redaction failed, not that PII leaked.

## Support bundle

Sidebar → **Operations** → **Support bundle** (`/ui/admin/support-bundle`). Produces an encrypted `.tar.gz` containing:

* Recent audit events (with redaction),
* Configuration snapshot (with secrets redacted),
* Licence metadata,
* Corpus version + migration state,
* Recent error logs.

The bundle is signed and encrypted; the ReSouris support engineer decrypts with their support key.

## Backup + restore

Backup adapters live under `adapters/backup_encryption/`. Choices:

* **Cloud KMS** (SaaS + cloud self-hosted) — data key wrapped by AWS KMS / Google Cloud KMS / Azure Key Vault.
* **Local PEM** (self-hosted, air-gapped) — data key wrapped by an on-disk PEM key. The PEM is stored on a separate volume from the backup destination.
* **Env-var provider** — dev / test only; not for production.

Restore requires the wrapping key and the backup blob. The restore path validates the audit chain against the seal at the time of backup — a corrupted backup that presents an intact-looking chain but bad seals is rejected.

## Regulatory framing

ReSouris's audit model is deliberately compatible with:

* **NHS Data Security and Protection Toolkit** — the tamper-evident chain + seals satisfy the assurance requirements.
* **CQC** — the "who did what and when" query pattern maps directly onto audit-page filters.
* **GDPR Article 30** — data-processing records queryable via the audit chain; retention is per-tenant configurable with a 7-year default for audit rows.
* **HIPAA** (if used in US deployments) — audit-trail requirement under §164.312(b) satisfied by the chain + seal model.

## Deliberate limitations for v1

* **S3 Object Lock witness** — stub only; graduates in v1.5+.
* **Cross-tenant reporting** — no platform-level query surface for "which tenants had what audit patterns"; deliberate isolation.
* **Real-time streaming to a SIEM** — Webhook-per-event exists as a v1.5+ enhancement, not shipped.
* **Deterministic PDF chart rendering for audit reports** — deferred.

The pattern: ship the parts that satisfy the compliance case; ship honest signals for the parts that don't; graduate the stubs when the operational case is made.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://help.resouris.com/part-2-running-resouris/audit-and-compliance.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
