Veefore docs

Audit and export

Verify the chain on a schedule, produce a verifiable evidence pack, and treat retention changes as the destructive operation they are.

#Verify on a schedule

bash
veefore audit verify

Exits non-zero if the chain is broken, and names the first position where the recomputed hash diverges. Over the API: POST /v1/audit-events/verify-integrity, requiring audit:verify_integrity.

Verify on a schedule, not after an incident

A check you only run when you suspect something tells you nothing about when the problem started. Run it daily, from CI or a cron job, with the result going somewhere a person reads.

Then a failure has a date attached, and the date narrows the investigation to a window instead of to all of history.

yaml
name: Veefore audit integrity
on:
  schedule: [{ cron: '0 6 * * *' }]
jobs:
  verify:
    runs-on: ubuntu-latest
    env:
      VEEFORE_CONTROL_PLANE_URL: ${{ vars.VEEFORE_CONTROL_PLANE_URL }} # self-hosted only
      VEEFORE_EMAIL: ${{ secrets.VEEFORE_AUDIT_EMAIL }}
      VEEFORE_PASSWORD: ${{ secrets.VEEFORE_AUDIT_PASSWORD }}
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm install -g veefore
      - run: veefore audit verify

An AUDITOR role is enough — it holds audit:read, audit:export and audit:verify_integrity and nothing that can change anything.

#Produce an evidence pack

bash
veefore audit export \
  --format csv \
  --from 2026-07-01T00:00:00Z \
  --to 2026-10-01T00:00:00Z \
  --reason "Quarterly SOC 2 evidence" \
  --out audit-2026-q3.csv

Three things happen:

  1. The reason is required and recorded.
  2. The export is itself an audit event. Removing evidence from the system is worth knowing happened.
  3. The file is watermarked and you get a content hash. An auditor recomputes the hash to prove the file they received is the file that was produced.

Written at mode 0600.

Give the auditor the hash separately

The hash proves the file is unaltered only if it reaches them through a different path than the file. Emailing both in one message proves nothing — whoever could alter one could alter the other.

Send the file however is convenient; send the hash over a channel that is independently authenticated.

Over the API: POST /v1/audit-events/export, requiring audit:export.

bash
veefore audit list --severity CRITICAL --from 2026-10-01T00:00:00Z
veefore audit list --type agent.revoked
veefore audit show evt_01ABC

Cursor-paginated over the API:

http
GET /v1/audit-events?severity=CRITICAL&cursor=…&limit=50
GET /v1/audit-events/{eventId}

The detail view includes the event's position in the hash chain.

#Severity, and what belongs in CRITICAL

INFO Routine state changes
NOTICE Worth seeing in a review
WARNING Something is off
CRITICAL A security team must see this

CRITICAL is reserved for: agent revocation, emergency controls, break-glass access, reducing enforcement, and changing audit retention.

If CRITICAL is noisy, something is miscategorised

The point of the severity is that a CRITICAL event is always worth a human look. A CRITICAL stream that gets skimmed is a CRITICAL stream that will be skimmed on the day it matters.

#Ship it somewhere else

Audit events exist to survive. The chain detects tampering within the stored sequence; it does not protect against an attacker with database write access and the chain secret, who could rewrite the whole chain consistently.

The mitigation is boring and effective:

  • Keep VEEFORE_AUDIT_CHAIN_SECRET out of the database and out of the application's own config store.
  • Restrict database write access to the application role.
  • Ship events to an append-only sink you control separately — object storage with versioning and object lock, or your SIEM.

Subscribe a webhook to the event types you care about, or export on a schedule. Either way the second copy is what turns "we think nothing was altered" into "two independent records agree".

See Audit integrity.

#Retention

Configurable, requiring audit:configure_retention — step-up gated, writing a CRITICAL audit event.

Shortening retention destroys evidence

That is why it is a high-impact, step-up-gated, CRITICAL-audited operation. A request to "clean up old audit data" leaves a mark explaining who asked and why.

Export and archive before you shorten:

bash
veefore audit export --format json \
  --reason "Pre-retention-change archive" --out archive.json

The archive is watermarked and hashed, so it remains verifiable afterwards.

#Correlation

Every API response carries x-request-id and x-trace-id. Both appear on the action record, on the audit event, and in the structured logs.

Given a request_id from a user complaint:

bash
veefore logs --limit 100              # the decision
curl -s $CP/v1/actions/act_01ABC …        # the full lineage
curl -s $CP/v1/audit-events/evt_01ABC …   # the audit event
# plus every log line, if you index on requestId

That is the intended path, and it is why the ids are on the response rather than only in the logs.

#What an export contains

For a protected action: the actor and its type, the resolved authority and delegation chain, every policy consulted with versions, every rule that matched, every risk signal with its weight and explanation, the approval and who decided it, the credential lease by reference, the downstream status, the outcome, and the correlation ids.

Never the credential material. Never a private key. Masked parameter paths are masked in audit too, because a MASK decision redacts the value in the trail as well as in the forwarded request.

Identity, authority, policy and enforcement for autonomous AI agents.