Audit ledger export

Your audit ledger is a hash-chained record: each entry carries a digest of its own content and of the entry before it. The export hands you those rows, with everything needed to check the chain yourself — on your own machine, with no call back to Trefur.

Pulling an export

Admin or owner only, and today a role reaches Trefur only from a signed-in session. In the dashboard that is Safety → Governance, the Scores tab, then Ledger, where Export CSV and Export NDJSON each download one page along with its receipt.

A workspace API key cannot pull this export. The /platform/v1 surface accepts API keys and only API keys, and an API key carries scopes, never a role — so a key-authenticated caller arrives as a viewer and is refused with 403. Every class of key behaves the same way, and no scope changes it. A scheduled archival job running on a key is not something you can build yet; an admin or owner pulling from a signed-in session is.

The parameters the export takes:

ParameterValuesDefault
formatcsv or ndjsoncsv
start_time, end_timeRFC 3339unbounded
limit1 – 50000 rows per page10000
afterThe cursor from the previous page's headernone

Only the first page of a pull is recorded in the ledger — the continuations belong to the same pull. The time range is carried inside the cursor, so a continuation cannot be steered outside the pull it started: passing a different start_time with an after cursor does nothing.

The request behind the dashboard's buttons is the one below. Use it if you are scripting the pull with a signed-in session — to follow the cursor past the first page, which the buttons do not do.

GET /api/observe/governance/ledger/export
      ?format=ndjson
      &start_time=2026-09-01T00:00:00Z
      &limit=50000

Origin:        your dashboard (app.trefur.com)
Authorization: the signed-in session's bearer token

Keep the response headers with the body you saved. They carry the
snapshot hash and the attestation, and the verifier needs them.

The receipt headers

HeaderMeaning
X-Trefur-Snapshot-Sha256SHA-256 over the exact bytes of this page's body.
X-Trefur-Export-RowsRows in this page.
X-Trefur-Export-Generated-AtWhen the page was rendered, to the second.
X-Trefur-Hash-FormulaThe hash construction the rows use — sha256-v1.
X-Trefur-AttestationThe attestation algorithm, or the literal unavailable.
X-Trefur-Attestation-HmacThe attestation itself, when one is available.
X-Trefur-Next-CursorPresent only when another page exists.

The attestation binds the workspace, the snapshot hash, the generation time and the row count together, so a page cannot be passed off as a different page, a different workspace or a different point in time. When the deployment holds no attestation key, the header says unavailable in plain words rather than publishing a placeholder that looks like a signature.

The attestation is a shared secret, not a public key. Checking it needs the key the deployment holds. If you are running Trefur yourself you have it; on Trefur Cloud, checking the attestation is something you ask us to do with you. The rest of the verification — the whole hash chain, and the body against the snapshot hash — needs no key at all and is the part that matters most.

What a row contains

NDJSON gives one object per line. CSV gives a header row and 19 columns: the nine the export has always had — sequence number, event type, severity, summary, agent, actor type, entry hash, previous hash, timestamp — followed by the ten that make it verifiable: chain scope, content digest, hash version, actor id, API key id, redaction time, the canonical form of the details object, the row id, the workspace, and the external agent id.

Each row carries a content digest over its summary and details, and an entry hash over the previous row's hash, the workspace, the sequence number, the event type, the severity, the timestamp and that content digest. A workspace has several parallel chains — the chain scope column says which one a row belongs to — and rows link to the row before them within their own scope.

Verifying it offline

The chain is verifiable entirely offline: every value the check needs is in the file you downloaded and its headers, and no part of it calls back to Trefur. The format is documented above, so you can write the check yourself if you would rather.

We also maintain a verifier, ledger-verify, which recomputes the chain from the file you hold — CSV or NDJSON, no network. It is available on request: ask us and we will send it to you. There is no published binary and no installer yet, and it is not part of the collector CLI.

# Chain only — no key needed, and no headers needed.
ledger-verify --file audit_ledger.ndjson

# Chain + the page's snapshot hash + the attestation.
ledger-verify --file audit_ledger.ndjson \
  --headers headers.txt \
  --attestation-key-file key.txt \
  --tenant <your-workspace-uuid>

It does this, in order:

  1. Groups the rows by chain scope and sorts each scope by sequence number, so a file whose rows arrived interleaved still verifies.
  2. Takes the first row of each scope as the anchor, and marks the scope partial when the export was a window rather than the beginning of the chain.
  3. Checks each row's previous-hash against the preceding row's entry hash, and rejects a duplicate sequence number.
  4. Recomputes the content digest from the summary and details, catching text that was edited after the fact — a check the server itself does not repeat, because it trusts its stored digest.
  5. Recomputes the entry hash and compares it with the stored one.
  6. With --headers, re-hashes the file you downloaded and compares it to the published snapshot hash; with a key and --tenant as well, re-derives and compares the attestation.

It exits 0 when everything it was asked to check verifies, 1 when something does not, and 2 when the input cannot be read at all. It prints the totals and a line per chain scope; --json gives the same result as a machine-readable object.

An empty file does not pass. Zero rows verifies nothing, and it says so rather than reporting success over nothing.

"Unverifiable" rows

Some old rows were written before content digests were stored, and have since been redacted. For those, neither the digest nor the text it was taken over still exists, so the entry hash cannot be recomputed. The verifier counts them as unverifiable: it reports them, never folds them into the verified count, and still checks the chain across them — the next row's previous-hash must equal their stored entry hash. The link across such a row is asserted rather than proven, which is exactly why they are counted separately instead of being waved through.

A redacted row that does carry a stored digest verifies normally: redaction removes the text on purpose, and the digest is what the chain was built on.

The per-scope line does not print the unverifiable count — use --json when you need it. A file with unverifiable rows can report valid with a verified count lower than its row count, and that is the discrepancy to look at.

Limits

  • Verification is per page. Each page anchors on its own first row and carries its own snapshot hash and attestation; there is no single attestation over the whole ledger. Keep each page's headers with its body.
  • A ranged export is a window, not a chain from its beginning. The verifier reports it as partial rather than treating it as broken.
  • The attestation is symmetric — it proves the export came from a holder of the key, and it does not name which key version signed it. Rotating the key does not invalidate earlier exports, which stay verifiable with the older key.
  • ledger-verify is available on request. There is no released binary, no installer, and it is not part of the collector CLI. The chain is verifiable without it.
  • No API key can pull this export. An unattended archival job is not possible today; an admin or owner pulling from a signed-in session is.
  • The dashboard's export buttons download the first page only, with no cursor follow.
  • The export reads the ledger. It cannot recover an entry that fell outside your workspace's retention, and it does not un-redact anything.

See also: the OCSF export for activity and findings in a SIEM-native schema.